/* * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.netconf.transport.http; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.opendaylight.netconf.transport.http.BasicAuthHandler.BASIC_AUTH_PREFIX; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.stream.Stream; import org.apache.commons.codec.digest.Crypt; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.crypt.hash.rev140806.CryptHash; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.HttpServerGrouping; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.ClientAuthentication; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.ClientAuthenticationBuilder; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.client.authentication.users.UserBuilder; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.client.authentication.users.user.auth.type.basic.BasicBuilder; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.grouping.client.authentication.users.user.auth.type.basic.basic.PasswordBuilder; import org.opendaylight.yangtools.yang.binding.util.BindingMap; public class BasicAuthHandlerTest { private static final String USERNAME1 = "username-1"; private static final String USERNAME2 = "username-2"; private static final String PASSWORD1 = "pa$$W0rd!1"; private static final String PASSWORD2 = "pa$$W0rd#2"; private static final String HASHED_PASSWORD2 = Crypt.crypt(PASSWORD2, "$6$rounds=4500$sha512salt"); private EmbeddedChannel channel; @BeforeEach void beforeEach() { final var authHandler = BasicAuthHandler.ofNullable(new HttpServerGrouping() { @Override public Class implementedInterface() { throw new UnsupportedOperationException(); } @Override public String getServerName() { return null; } @Override public ClientAuthentication getClientAuthentication() { final var user1 = new UserBuilder() .setUserId(USERNAME1) .setAuthType(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208 .http.server.grouping.client.authentication.users.user.auth.type.BasicBuilder() .setBasic(new BasicBuilder() .setUsername(USERNAME1) .setPassword(new PasswordBuilder() .setHashedPassword(new CryptHash("$0$" + PASSWORD1)) .build()) .build()) .build()) .build(); final var user2 = new UserBuilder() .setUserId(USERNAME2) .setAuthType(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208 .http.server.grouping.client.authentication.users.user.auth.type.BasicBuilder() .setBasic(new BasicBuilder() .setUsername(USERNAME2) .setPassword(new PasswordBuilder() .setHashedPassword(new CryptHash(HASHED_PASSWORD2)) .build()) .build()) .build()) .build(); return new ClientAuthenticationBuilder() .setUsers(new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208 .http.server.grouping.client.authentication.UsersBuilder() .setUser(BindingMap.of(user1, user2)).build()).build(); } }); assertNotNull(authHandler); channel = new EmbeddedChannel(authHandler); } @ParameterizedTest(name = "BasicAuth success: {0} password configured") @MethodSource("authSuccessArgs") void authSuccess(final String testDesc, final String username, final String password) { final String authHeader = authHeader(BASIC_AUTH_PREFIX, username, password); final var request = newHttpRequest(authHeader); channel.writeInbound(request); // nonnull read indicates the message is passed for next handler assertEquals(request, channel.readInbound()); } private static Stream authSuccessArgs() { return Stream.of( // test descriptor, username, password Arguments.of("unencrypted", USERNAME1, PASSWORD1), Arguments.of("sha512 encrypted", USERNAME2, PASSWORD2)); } @ParameterizedTest(name = "BasicAuth failure: {0}") @MethodSource("authFailureArgs") void authFailure(final String testDesc, final String authHeader) { channel.writeInbound(newHttpRequest(authHeader)); // null indicates the request is consumed and not passed to next handler assertNull(channel.readInbound()); // verify response final var outbound = channel.readOutbound(); assertNotNull(outbound); final var response = assertInstanceOf(HttpResponse.class, outbound); assertEquals(HttpResponseStatus.UNAUTHORIZED, response.status()); } private static Stream authFailureArgs() { return Stream.of( // test descriptor, auth header Arguments.of("no Authorization header", null), Arguments.of("Authorization header does not start with `Basic`", "Bearer ABCD+"), Arguments.of("Base64 decode failure", BASIC_AUTH_PREFIX + "cannot-decode-this"), Arguments.of("No expected username:password", BASIC_AUTH_PREFIX + Base64.getEncoder().encodeToString("abcd".getBytes(StandardCharsets.UTF_8))), Arguments.of("Unknown user", authHeader(BASIC_AUTH_PREFIX, "unknown", "user")), Arguments.of("Wrong password", authHeader(BASIC_AUTH_PREFIX, USERNAME1, PASSWORD2))); } private static String authHeader(final String prefix, final String username, final String password) { return prefix + Base64.getEncoder() .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); } private static HttpRequest newHttpRequest(final String authHeader) { final var request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/uri"); if (authHeader != null) { request.headers().add(HttpHeaderNames.AUTHORIZATION, authHeader); } return request; } }