Created
November 8, 2016 15:32
-
-
Save anuraaga/21e69c8a5e826e752ebaa62668f26563 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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