mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-05 22:28:45 +02:00
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.
This commit is contained in:
2
gradle/changelog/basic_auth_with_access_token.yaml
Normal file
2
gradle/changelog/basic_auth_with_access_token.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Support basic authentication with access token ([#1694](https://github.com/scm-manager/scm-manager/pulls/1694))
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user