Skip to content

Instantly share code, notes, and snippets.

@anuraaga
Created November 8, 2016 15:32
Show Gist options
  • Save anuraaga/21e69c8a5e826e752ebaa62668f26563 to your computer and use it in GitHub Desktop.
Save anuraaga/21e69c8a5e826e752ebaa62668f26563 to your computer and use it in GitHub Desktop.
import com.google.api.client.util.Strings;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.linecorp.armeria.common.http.AggregatedHttpMessage;
import com.linecorp.armeria.common.http.DefaultHttpResponse;
import com.linecorp.armeria.common.http.HttpHeaderNames;
import com.linecorp.armeria.common.http.HttpHeaders;
import com.linecorp.armeria.common.http.HttpRequest;
import com.linecorp.armeria.common.http.HttpResponse;
import com.linecorp.armeria.common.http.HttpStatus;
import com.linecorp.armeria.server.DecoratingService;
import com.linecorp.armeria.server.Service;
import com.linecorp.armeria.server.ServiceRequestContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.io.IOException;
import java.util.Base64;
import javax.inject.Inject;
import javax.inject.Named;
import lombok.Value;
/**
* Decorates a services to enable basic authentication.
* Basic authentication should be used only under https
* because password in basic authentication isn't encrypted.
*/
public class BasicAuthService
extends DecoratingService<HttpRequest, HttpResponse, HttpRequest, HttpResponse> {
@Named
@Lazy
public static class Factory {
private final BasicAuthChecker basicAuthChecker;
private final BasicAuthSettings basicAuthSettings;
@Inject
public Factory(BasicAuthChecker basicAuthChecker, BasicAuthSettings basicAuthSettings) {
this.basicAuthChecker = basicAuthChecker;
this.basicAuthSettings = basicAuthSettings;
}
public Service<HttpRequest, HttpResponse> decorate(
Service<? super HttpRequest, ? extends HttpResponse> delegate) {
return new BasicAuthService(delegate, basicAuthChecker, basicAuthSettings);
}
}
private final BasicAuthChecker basicAuthChecker;
private final BasicAuthSettings basicAuthSettings;
private static final String AUTHORIZATION_PREFIX = "Basic ";
/**
* Creates a new instance that decorates the specified {@link Service}.
*/
@VisibleForTesting
BasicAuthService(
Service<? super HttpRequest, ? extends HttpResponse> delegate,
BasicAuthChecker basicAuthChecker,
BasicAuthSettings basicAuthSettings) {
super(delegate);
this.basicAuthSettings = basicAuthSettings;
this.basicAuthChecker = basicAuthChecker;
assert basicAuthSettings != null && !Strings.isNullOrEmpty(basicAuthSettings.getRealm());
}
@Override
public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
String header = req.headers().get(HttpHeaderNames.AUTHORIZATION);
if (header == null || !header.startsWith(AUTHORIZATION_PREFIX)) {
DefaultHttpResponse response = new DefaultHttpResponse();
response.respond(AggregatedHttpMessage.of(
HttpHeaders.of(HttpStatus.UNAUTHORIZED)
.set(HttpHeaderNames.WWW_AUTHENTICATE,
AUTHORIZATION_PREFIX + "realm=\"" + basicAuthSettings.getRealm() + "\"")));
return response;
}
try {
BasicAuthCredential credential = extractAndDecodeHeader(header);
if (basicAuthChecker.isAuthenticated(credential.getUserName(), credential.getPassword())) {
UsernamePasswordAuthenticationToken
authentication =
new UsernamePasswordAuthenticationToken(credential.getUserName(),
credential.getPassword());
ctx.onEnter(() -> SecurityContextHolder.getContext().setAuthentication(authentication));
ctx.onExit(SecurityContextHolder::clearContext);
return delegate().serve(ctx, req);
}
} finally {
SecurityContextHolder.clearContext();
}
DefaultHttpResponse response = new DefaultHttpResponse();
response.respond(HttpStatus.UNAUTHORIZED);
return response;
}
@Value(staticConstructor = "of")
static class BasicAuthCredential {
String userName;
String password;
}
/**
* Decodes the header into a username and password.
* @throws BadCredentialsException if the Basic header is not present or is not valid Base64
*/
@VisibleForTesting
BasicAuthCredential extractAndDecodeHeader(String header)
throws IOException {
byte[] base64Token = header.substring(AUTHORIZATION_PREFIX.length()).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException e) {
throw new BadCredentialsException(
"Failed to decode basic authentication token");
}
String token = new String(decoded, Charsets.UTF_8);
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return BasicAuthCredential.of(token.substring(0, delim), token.substring(delim + 1));
}
}
/**
* Checks whether a given set of user name and password is valid
*/
public interface BasicAuthChecker {
boolean isAuthenticated(String userName, String password);
}
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Lazy;
import javax.inject.Named;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* Settings for Basic Authentication
* @see <a href="https://tools.ietf.org/html/rfc2617">RFC 2617 - HTTP Authentication</a>
*/
@Accessors(chain = true)
@Data
@Named
@Lazy
@ConfigurationProperties(prefix = "basic-auth")
public class BasicAuthSettings {
private String realm;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment