From 965b5dbcedd8c089ac28155379ed568e4313e648 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 10 Jun 2021 08:27:01 +0200 Subject: [PATCH] Add support for basic authentication with access token (#1694) A special user __bearer_token with a valid access token as password can be used with basic authentication. --- .../basic_auth_with_access_token.yaml | 2 + .../sonia/scm/web/BasicWebTokenGenerator.java | 68 +++++---- .../scm/web/BasicWebTokenGeneratorTest.java | 136 ++++++++---------- 3 files changed, 98 insertions(+), 108 deletions(-) create mode 100644 gradle/changelog/basic_auth_with_access_token.yaml diff --git a/gradle/changelog/basic_auth_with_access_token.yaml b/gradle/changelog/basic_auth_with_access_token.yaml new file mode 100644 index 0000000000..67e43450ae --- /dev/null +++ b/gradle/changelog/basic_auth_with_access_token.yaml @@ -0,0 +1,2 @@ +- type: added + description: Support basic authentication with access token ([#1694](https://github.com/scm-manager/scm-manager/pulls/1694)) diff --git a/scm-webapp/src/main/java/sonia/scm/web/BasicWebTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/web/BasicWebTokenGenerator.java index 36ab064f10..b546c51f56 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/BasicWebTokenGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/web/BasicWebTokenGenerator.java @@ -21,31 +21,31 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.base.Charsets; +import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; - -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; - +import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.codec.Base64; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.Priority; import sonia.scm.plugin.Extension; +import sonia.scm.security.BearerToken; +import sonia.scm.security.SessionId; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +//~--- JDK imports ------------------------------------------------------------ /** * Creates a {@link UsernamePasswordToken} from an authorization header with @@ -56,15 +56,17 @@ import javax.servlet.http.HttpServletRequest; */ @Priority(100) @Extension -public class BasicWebTokenGenerator extends SchemeBasedWebTokenGenerator -{ +public class BasicWebTokenGenerator extends SchemeBasedWebTokenGenerator { + + @VisibleForTesting + static final String BEARER_TOKEN_IDENTIFIER = "__bearer_token"; /** credential separator for basic authentication */ private static final String CREDENTIAL_SEPARATOR = ":"; /** default encoding to decode basic authentication header */ - private static final Charset DEFAULT_ENCODING = Charsets.ISO_8859_1; - + private static final Charset DEFAULT_ENCODING = StandardCharsets.ISO_8859_1; + /** * the logger for BasicWebTokenGenerator */ @@ -95,10 +97,9 @@ public class BasicWebTokenGenerator extends SchemeBasedWebTokenGenerator * @return {@link UsernamePasswordToken} or {@code null} */ @Override - protected UsernamePasswordToken createToken(HttpServletRequest request, - String scheme, String authorization) + protected AuthenticationToken createToken(HttpServletRequest request, String scheme, String authorization) { - UsernamePasswordToken authToken = null; + AuthenticationToken authToken = null; if (HttpUtil.AUTHORIZATION_SCHEME_BASIC.equalsIgnoreCase(scheme)) { @@ -111,15 +112,7 @@ public class BasicWebTokenGenerator extends SchemeBasedWebTokenGenerator String username = token.substring(0, index); String password = token.substring(index + 1); - if (Util.isNotEmpty(username) && Util.isNotEmpty(password)) - { - logger.trace("try to authenticate user {}", username); - authToken = new UsernamePasswordToken(username, password); - } - else if (logger.isWarnEnabled()) - { - logger.warn("username or password is null/empty"); - } + authToken = createTokenFromCredentials(request, username, password); } else if (logger.isWarnEnabled()) { @@ -129,11 +122,26 @@ public class BasicWebTokenGenerator extends SchemeBasedWebTokenGenerator return authToken; } - -/** + + private AuthenticationToken createTokenFromCredentials(HttpServletRequest request, String username, String password) { + if (Util.isNotEmpty(username) && Util.isNotEmpty(password)) { + if (BEARER_TOKEN_IDENTIFIER.equals(username)) { + logger.trace("create bearer token"); + return BearerToken.create(SessionId.from(request).orElse(null), password); + } else { + logger.trace("create username password token for {}", username); + return new UsernamePasswordToken(username, password); + } + } else if (logger.isWarnEnabled()) { + logger.warn("username or password is null/empty"); + } + return null; + } + + /** * Decode base64 of the basic authentication header. The method will use - * the charset provided by the {@link UserAgent}, if the - * {@link UserAgentParser} is not available the method will be fall back to + * the charset provided by the {@link UserAgent}, if the + * {@link UserAgentParser} is not available the method will be fall back to * ISO-8859-1. * * @param request http request diff --git a/scm-webapp/src/test/java/sonia/scm/web/BasicWebTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/web/BasicWebTokenGeneratorTest.java index efa7086dc0..061cd8f8dd 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/BasicWebTokenGeneratorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/BasicWebTokenGeneratorTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- @@ -29,116 +29,96 @@ package sonia.scm.web; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.codec.Base64; - -import org.junit.Test; -import org.junit.runner.RunWith; - +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.security.BearerToken; import javax.servlet.http.HttpServletRequest; -import org.junit.Before; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +//~--- JDK imports ------------------------------------------------------------ /** * TODO add test with {@link UserAgentParser}. - * + * * @author Sebastian Sdorra */ -@RunWith(MockitoJUnitRunner.class) -public class BasicWebTokenGeneratorTest -{ - - /** - * Set up object under test. - * Use {@code null} as {@link UserAgentParser}. - */ - @Before - public void setUpObjectUnderTest() { +@ExtendWith(MockitoExtension.class) +class BasicWebTokenGeneratorTest { + + private BasicWebTokenGenerator generator; + + @Mock + private HttpServletRequest request; + + @BeforeEach + void setUpObjectUnderTest() { generator = new BasicWebTokenGenerator(null); } - /** - * Method description - * - */ @Test - public void testCreateToken() - { + void shouldCreateUsernamePasswordToken() { String trillian = Base64.encodeToString("trillian:secret".getBytes()); - when(request.getHeader("Authorization")).thenReturn( - "Basic ".concat(trillian)); + when(request.getHeader("Authorization")).thenReturn("Basic ".concat(trillian)); AuthenticationToken token = generator.createToken(request); - - assertThat(token, instanceOf(UsernamePasswordToken.class)); - - UsernamePasswordToken upt = (UsernamePasswordToken) token; - - assertEquals("trillian", token.getPrincipal()); - assertArrayEquals("secret".toCharArray(), upt.getPassword()); + assertThat(token) + .isInstanceOfSatisfying(UsernamePasswordToken.class, usernamePasswordToken -> { + assertThat(usernamePasswordToken.getPrincipal()).isEqualTo("trillian"); + assertThat(usernamePasswordToken.getPassword()).isEqualTo("secret".toCharArray()); + }); } - /** - * Method description - * - */ @Test - public void testCreateTokenWithWrongAuthorizationHeader() - { + void shouldCreateBearerToken() { + String bearerToken = Base64.encodeToString( + (BasicWebTokenGenerator.BEARER_TOKEN_IDENTIFIER + ":awesome_access_token").getBytes() + ); + + when(request.getHeader("Authorization")).thenReturn("Basic ".concat(bearerToken)); + + assertThat(generator.createToken(request)) + .isInstanceOfSatisfying( + BearerToken.class, + token -> assertThat(token.getCredentials()).isEqualTo("awesome_access_token") + ); + } + + @Test + void shouldNotCreateTokenWithWrongAuthorizationHeader() { when(request.getHeader("Authorization")).thenReturn("NONBASIC ASD"); - assertNull(generator.createToken(request)); + + AuthenticationToken token = generator.createToken(request); + assertThat(token).isNull(); } - /** - * Method description - * - */ @Test - public void testCreateTokenWithWrongBasicAuthorizationHeader() - { + void shouldNotCreateTokenWithWrongBasicAuthorizationHeader() { when(request.getHeader("Authorization")).thenReturn("Basic ASD"); - assertNull(generator.createToken(request)); + + AuthenticationToken token = generator.createToken(request); + assertThat(token).isNull(); } - /** - * Method description - * - */ @Test - public void testCreateTokenWithoutAuthorizationHeader() - { - assertNull(generator.createToken(request)); + void testCreateTokenWithoutAuthorizationHeader() { + AuthenticationToken token = generator.createToken(request); + assertThat(token).isNull(); } - /** - * Method description - * - */ @Test - public void testCreateTokenWithoutPassword() - { + void shouldNotCreateTokenWithoutPassword() { String trillian = Base64.encodeToString("trillian:".getBytes()); + when(request.getHeader("Authorization")).thenReturn("Basic ".concat(trillian)); - when(request.getHeader("Authorization")).thenReturn( - "Basic ".concat(trillian)); - assertNull(generator.createToken(request)); + AuthenticationToken token = generator.createToken(request); + assertThat(token).isNull(); } - //~--- fields --------------------------------------------------------------- - - private BasicWebTokenGenerator generator; - - /** Field description */ - @Mock - private HttpServletRequest request; }