Skip to content
Snippets Groups Projects
Commit 5fa8c871 authored by Evgeny Bardin's avatar Evgeny Bardin
Browse files

OZG-7092 refactor token service

parent 291350fa
No related branches found
No related tags found
1 merge request!1OZG-7092 Anpassung TokenChecker
......@@ -22,6 +22,7 @@ package de.ozgcloud.token;
import java.util.List;
import de.ozgcloud.token.common.errorhandling.ValidationError;
import lombok.Builder;
import lombok.Getter;
import lombok.Singular;
......@@ -31,10 +32,8 @@ import lombok.Singular;
public class TokenValidationResult {
private final boolean valid;
private final String postfachId;
private final String trustLevel;
private final TokenAttributes attributes;
@Singular
private final List<TokenAttribute> attributes;
private final String errorMesssage;
private final List<ValidationError> validationErrors;
}
......@@ -26,10 +26,9 @@ package de.ozgcloud.token.saml;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.saml.saml2.core.Issuer;
......@@ -38,7 +37,7 @@ import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
import org.springframework.stereotype.Service;
import de.ozgcloud.common.errorhandling.TechnicalException;
import de.ozgcloud.token.TokenAttribute;
import de.ozgcloud.token.TokenAttributes;
import de.ozgcloud.token.TokenValidationResult;
import de.ozgcloud.token.common.errorhandling.TokenVerificationException;
import lombok.RequiredArgsConstructor;
......@@ -57,15 +56,16 @@ public class SamlTokenService {
public TokenValidationResult validate(String token) {
try {
return buildValidTokenResult(validate(parseToken(token)));
return buildValidTokenResult(getAttributes(parseToken(token)));
} catch (TokenVerificationException e) {
LOG.debug("Token validation failed", e);
e.getValidationErrors().forEach(validationError -> LOG.error(validationError.getMessage(), validationError.getCause()));
return buildInvalidTokenResult(e);
}
}
Response parseToken(String token) {
try (var inputStream = new ByteArrayInputStream(token.getBytes(StandardCharsets.UTF_8));) {
try (var inputStream = buildInputStream(token)) {
var element = parserPool.parse(inputStream).getDocumentElement();
return (Response) responseUnmarshaller.unmarshall(element);
} catch (IOException | XMLParserException | UnmarshallingException e) {
......@@ -73,14 +73,12 @@ public class SamlTokenService {
}
}
Set<TokenAttribute> validate(Response token) {
var tokenIssuer = getTokenIssuer(token);
return getValidationService(tokenIssuer).validate(token);
InputStream buildInputStream(String token) {
return new ByteArrayInputStream(token.getBytes(StandardCharsets.UTF_8));
}
SamlAttributeService getValidationService(String tokenIssuer) {
return samlServiceRegistry.getService(tokenIssuer)
.orElseThrow(() -> new TechnicalException("No validation service found for issuer %s".formatted(tokenIssuer)));
TokenAttributes getAttributes(Response token) {
return getSamlAttributeService(getTokenIssuer(token)).getAttributes(token);
}
String getTokenIssuer(Response token) {
......@@ -88,24 +86,19 @@ public class SamlTokenService {
.orElseThrow(() -> new TokenVerificationException("No token issuer found"));
}
TokenValidationResult buildValidTokenResult(Collection<TokenAttribute> tokenAttributes) {
var resultBuilder = TokenValidationResult.builder().valid(true);
for (var attr : tokenAttributes) {
if (attr.isPostfachId()) {
resultBuilder.postfachId(attr.getValue());
} else if (attr.isTrustLevel()) {
resultBuilder.trustLevel(attr.getValue());
} else {
resultBuilder.attribute(attr);
}
SamlAttributeService getSamlAttributeService(String tokenIssuer) {
return samlServiceRegistry.getService(tokenIssuer)
.orElseThrow(() -> new TechnicalException("Can't validate token with issuer %s".formatted(tokenIssuer)));
}
return resultBuilder.build();
TokenValidationResult buildValidTokenResult(TokenAttributes tokenAttributes) {
return TokenValidationResult.builder().valid(true).attributes(tokenAttributes).build();
}
TokenValidationResult buildInvalidTokenResult(TokenVerificationException exception) {
return TokenValidationResult.builder()
.valid(false)
.errorMesssage(exception.getMessage())
.validationErrors(exception.getValidationErrors())
.build();
}
......
......@@ -20,38 +20,34 @@
package de.ozgcloud.token;
import java.util.List;
import java.util.UUID;
import com.thedeanda.lorem.LoremIpsum;
import de.ozgcloud.token.TokenValidationResult.TokenValidationResultBuilder;
import de.ozgcloud.token.common.errorHandler.ValidationErrorTestFactory;
import de.ozgcloud.token.common.errorhandling.ValidationError;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CheckTokenResultTestFactory {
public class TokenValidationResultTestFactory {
public static final String POSTFACH_ID = UUID.randomUUID().toString();
public static final String TRUST_LEVEL = "LOW";
public static final TokenAttribute OTHER_FIELD = TokenAttributeTestFactory.create();
public static final String ERROR_MESSAGE = LoremIpsum.getInstance().getWords(4);
public static final TokenAttributes TOKEN_ATTRIBUTES = TokenAttributesTestFactory.create();
public static final ValidationError VALIDATION_ERROR = ValidationErrorTestFactory.create();
static TokenValidationResult createValid() {
return createBuilder().valid(true).build();
public static TokenValidationResult createValid() {
return createValidBuilder().valid(true).build();
}
public static TokenValidationResultBuilder createBuilder() {
public static TokenValidationResultBuilder createValidBuilder() {
return TokenValidationResult.builder()
.postfachId(POSTFACH_ID)
.trustLevel(TRUST_LEVEL)
.attributes(List.of(OTHER_FIELD));
.valid(true)
.attributes(TOKEN_ATTRIBUTES);
}
public static TokenValidationResult createInvalid() {
return createBuilder()
return createInvalidBuilder().build();
}
public static TokenValidationResult.TokenValidationResultBuilder createInvalidBuilder() {
return TokenValidationResult.builder()
.valid(false)
.errorMesssage(ERROR_MESSAGE)
.build();
.validationError(VALIDATION_ERROR);
}
}
/*
* Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
* Ministerpräsidenten des Landes Schleswig-Holstein
* Staatskanzlei
* Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.token.common.errorHandler;
import com.thedeanda.lorem.LoremIpsum;
import de.ozgcloud.token.common.errorhandling.ValidationError;
public class ValidationErrorTestFactory {
private static final String ERROR_MESSAGE = LoremIpsum.getInstance().getWords(4);
private static final Exception CAUSE = new Exception();
public static ValidationError create() {
return createBuilder().build();
}
public static ValidationError.ValidationErrorBuilder createBuilder() {
return ValidationError.builder()
.message(ERROR_MESSAGE)
.cause(CAUSE);
}
}
......@@ -23,16 +23,45 @@
*/
package de.ozgcloud.token.saml;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.Response;
import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.thedeanda.lorem.LoremIpsum;
class SamlTokenServiceTest {
import de.ozgcloud.common.errorhandling.TechnicalException;
import de.ozgcloud.token.TokenAttributes;
import de.ozgcloud.token.TokenAttributesTestFactory;
import de.ozgcloud.token.TokenValidationResult;
import de.ozgcloud.token.TokenValidationResultTestFactory;
import de.ozgcloud.token.common.errorHandler.ValidationErrorTestFactory;
import de.ozgcloud.token.common.errorhandling.TokenVerificationException;
import lombok.SneakyThrows;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import net.shibboleth.utilities.java.support.xml.XMLParserException;
private static final String SAML_TOKEN = LoremIpsum.getInstance().getWords(7);
class SamlTokenServiceTest {
@Spy
@InjectMocks
......@@ -40,9 +69,379 @@ class SamlTokenServiceTest {
@Mock
private SamlServiceRegistry samlServiceRegistry;
@Mock
private ParserPool parserPool;
@Mock
private ResponseUnmarshaller responseUnmarshaller;
@Nested
class TestValidate {
private static final String SAML_TOKEN = LoremIpsum.getInstance().getWords(7);
@Mock
private Response parsedToken;
@Nested
class TestValid {
private static final TokenValidationResult VALIDATION_RESULT = TokenValidationResultTestFactory.createValid();
private static final TokenAttributes TOKEN_ATTRIBUTES = TokenAttributesTestFactory.create();
@BeforeEach
void init() {
doReturn(parsedToken).when(service).parseToken(anyString());
doReturn(TOKEN_ATTRIBUTES).when(service).getAttributes(any());
doReturn(VALIDATION_RESULT).when(service).buildValidTokenResult(any());
}
@Test
void shouldCallParseToken() {
validate();
verify(service).validate(SAML_TOKEN);
}
@Test
void shouldCallGetAttributes() {
validate();
verify(service).getAttributes(parsedToken);
}
@Test
void shouldCallBuildValidTokenResult() {
validate();
verify(service).buildValidTokenResult(TOKEN_ATTRIBUTES);
}
@Test
void shouldReturnValidResult() {
var result = validate();
assertThat(result).isSameAs(VALIDATION_RESULT);
}
}
@Nested
class TestInvalid {
@Mock
private TokenVerificationException exception;
@DisplayName("should call buildInvalidTokenResult if parseToken throws exception")
@Test
void shouldCallBuildInvalidTokenResult1() {
doThrow(exception).when(service).parseToken(anyString());
validate();
verify(service).buildInvalidTokenResult(exception);
}
@DisplayName("should call buildInvalidTokenResult if getAttributes throws exception")
@Test
void shouldCallBuildInvalidTokenResult2() {
doReturn(parsedToken).when(service).parseToken(anyString());
doThrow(exception).when(service).getAttributes(any());
validate();
verify(service).buildInvalidTokenResult(exception);
}
@Test
void shouldReturnInvalidResult() {
doThrow(exception).when(service).parseToken(anyString());
var invalidTokenResult = TokenValidationResultTestFactory.createInvalid();
doReturn(invalidTokenResult).when(service).buildInvalidTokenResult(exception);
var result = validate();
assertThat(result).isSameAs(invalidTokenResult);
}
}
private TokenValidationResult validate() {
return service.validate(SAML_TOKEN);
}
}
@Nested
class TestParseToken {
private static final String SAML_TOKEN = LoremIpsum.getInstance().getWords(7);
@Mock
private Response response;
@Mock
private InputStream tokenStream;
@Mock
private Document tokenDocument;
@Mock
private Element tokenElement;
@BeforeEach
void init() {
doReturn(tokenStream).when(service).buildInputStream(anyString());
}
@Nested
class TestParsingSuccessfully {
@Captor
private ArgumentCaptor<InputStream> tokenStreamCaptor;
@SneakyThrows
@BeforeEach
void init() {
when(tokenDocument.getDocumentElement()).thenReturn(tokenElement);
when(parserPool.parse(any(InputStream.class))).thenReturn(tokenDocument);
}
@Test
void shouldCallBuildInputStream() {
parseToken();
verify(service).buildInputStream(SAML_TOKEN);
}
@SneakyThrows
@Test
void shouldCallParserPool() {
parseToken();
verify(parserPool).parse(tokenStreamCaptor.capture());
assertThat(tokenStreamCaptor.getValue()).isSameAs(tokenStream);
}
@SneakyThrows
@Test
void shouldCallUnmarshall() {
parseToken();
verify(responseUnmarshaller).unmarshall(tokenElement);
}
@SneakyThrows
@Test
void shouldReturnResponse() {
doReturn(response).when(responseUnmarshaller).unmarshall(any());
var result = parseToken();
assertThat(result).isSameAs(response);
}
}
@Nested
class TestParsingFails {
@SneakyThrows
@DisplayName("should throw TokenVerificationException on IOException")
@Test
void shouldThrowOnIOException() {
when(tokenDocument.getDocumentElement()).thenReturn(tokenElement);
when(parserPool.parse(any(InputStream.class))).thenReturn(tokenDocument);
var exception = new IOException();
doThrow(exception).when(tokenStream).close();
Assertions.assertThatThrownBy(TestParseToken.this::parseToken)
.isInstanceOf(TokenVerificationException.class)
.hasCause(exception);
}
@SneakyThrows
@DisplayName("should throw TokenVerificationException on XMLParserException")
@Test
void shouldThrowOnXMLParserException() {
var exception = new XMLParserException();
doThrow(exception).when(parserPool).parse(any(InputStream.class));
Assertions.assertThatThrownBy(TestParseToken.this::parseToken)
.isInstanceOf(TokenVerificationException.class)
.hasCause(exception);
}
@SneakyThrows
@DisplayName("should throw TokenVerificationException on UnmarshallingException")
@Test
void shouldThrowOnUnmarshallingException() {
when(tokenDocument.getDocumentElement()).thenReturn(tokenElement);
when(parserPool.parse(any(InputStream.class))).thenReturn(tokenDocument);
var exception = new UnmarshallingException();
doThrow(exception).when(responseUnmarshaller).unmarshall(any());
Assertions.assertThatThrownBy(TestParseToken.this::parseToken)
.isInstanceOf(TokenVerificationException.class)
.hasCause(exception);
}
}
private Response parseToken() {
return service.parseToken(SAML_TOKEN);
}
}
@Nested
class TestBuildInputStream {
private static final String SAML_TOKEN = LoremIpsum.getInstance().getWords(7);
@SneakyThrows
@Test
void shouldReturnInputStream() {
var result = service.buildInputStream(SAML_TOKEN);
assertThat(result.readAllBytes()).isEqualTo(SAML_TOKEN.getBytes());
}
}
@Nested
class TestGetAttributes {
private static final String TOKEN_ISSUER = LoremIpsum.getInstance().getWords(3);
private static final TokenAttributes TOKEN_ATTRIBUTES = TokenAttributesTestFactory.create();
@Mock
private Response token;
@Mock
private SamlAttributeService samlAttributeService;
@BeforeEach
void init() {
doReturn(TOKEN_ISSUER).when(service).getTokenIssuer(any());
doReturn(samlAttributeService).when(service).getSamlAttributeService(any());
doReturn(TOKEN_ATTRIBUTES).when(samlAttributeService).getAttributes(any());
}
@Test
void shouldCallGetTokenIssuer() {
getAttributes();
verify(service).getTokenIssuer(token);
}
@Test
void shouldCallGetValidationService() {
getAttributes();
verify(service).getSamlAttributeService(TOKEN_ISSUER);
}
@Test
void shouldCallGetSamlAttributeService() {
getAttributes();
verify(service).getSamlAttributeService(TOKEN_ISSUER);
}
@Test
void shouldCallGetAttributes() {
getAttributes();
verify(samlAttributeService).getAttributes(token);
}
@Test
void shouldReturnTokenAttributes() {
var result = getAttributes();
assertThat(result).isSameAs(TOKEN_ATTRIBUTES);
}
private TokenAttributes getAttributes() {
return service.getAttributes(token);
}
}
@Nested
class TestGetTokenIssuer {
private static final String TOKEN_ISSUER = LoremIpsum.getInstance().getWords(3);
@Mock
private Response token;
@Mock
private Issuer issuer;
@Test
void shouldReturnTokenIssuer() {
when(token.getIssuer()).thenReturn(issuer);
when(issuer.getValue()).thenReturn(TOKEN_ISSUER);
var result = service.getTokenIssuer(token);
assertThat(result).isEqualTo(TOKEN_ISSUER);
}
@Test
void shouldThrowOnNoIssuer() {
when(token.getIssuer()).thenReturn(null);
assertThatThrownBy(() -> service.getTokenIssuer(token)).isInstanceOf(TokenVerificationException.class);
}
}
@Nested
class TestGetSamlAttributeService {
private static final String TOKEN_ISSUER = LoremIpsum.getInstance().getWords(1);
@Mock
private SamlAttributeService samlAttributeService;
@Test
void shouldCallGetService() {
when(samlServiceRegistry.getService(anyString())).thenReturn(Optional.of(samlAttributeService));
getSamlAttributeService();
verify(samlServiceRegistry).getService(TOKEN_ISSUER);
}
@Test
void shouldReturnService() {
when(samlServiceRegistry.getService(anyString())).thenReturn(Optional.of(samlAttributeService));
var result = getSamlAttributeService();
assertThat(result).isSameAs(samlAttributeService);
}
@Test
void shouldThrowOnNoService() {
when(samlServiceRegistry.getService(anyString())).thenReturn(Optional.empty());
assertThatThrownBy(this::getSamlAttributeService).isInstanceOf(TechnicalException.class);
}
private SamlAttributeService getSamlAttributeService() {
return service.getSamlAttributeService(TOKEN_ISSUER);
}
}
@Nested
class TestBuildValidTokenResult {
@Test
void shouldBuildResult() {
var result = service.buildValidTokenResult(TokenAttributesTestFactory.create());
assertThat(result).usingRecursiveComparison().isEqualTo(TokenValidationResultTestFactory.createValid());
}
}
@Nested
class TestBuildInvalidTokenResult {
@Test
void shouldBuildResult() {
var exception = new TokenVerificationException("msg", List.of(ValidationErrorTestFactory.create()));
var result = service.buildInvalidTokenResult(exception);
assertThat(result).usingRecursiveComparison().isEqualTo(TokenValidationResultTestFactory.createInvalid());
}
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment