Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,8 @@ public Response finishOAuth2Challenge(String state, String code, URI callbackUri
if (handlerState.isEmpty()) {
Response.ResponseBuilder builder = Response
.seeOther(URI.create(UI_LOCATION))
.cookie(
OAuthWebUiCookie.create(tokenPairSerializer.serialize(fromOAuth2Response(oauth2Response)), cookieExpirationTime),
NonceCookie.delete());
.cookie(OAuthWebUiCookie.create(tokenPairSerializer.serialize(fromOAuth2Response(oauth2Response)), cookieExpirationTime))
.cookie(NonceCookie.delete());
if (oauth2Response.getIdToken().isPresent()) {
builder.cookie(OAuthIdTokenCookie.create(oauth2Response.getIdToken().get(), cookieExpirationTime));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.security.SecureRandom;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

Expand Down Expand Up @@ -70,6 +71,8 @@ public class FormWebUiAuthenticationFilter
private final FormAuthenticator formAuthenticator;
private final Optional<Authenticator> authenticator;

private static final MultipartUiCookie MULTIPART_COOKIE = new MultipartUiCookie(TRINO_UI_COOKIE, "/ui");

@Inject
public FormWebUiAuthenticationFilter(
FormWebUiConfig config,
Expand Down Expand Up @@ -225,21 +228,16 @@ public static ResponseBuilder redirectFromSuccessfulLoginResponse(String redirec
return Response.seeOther(redirectLocation);
}

public Optional<NewCookie> checkLoginCredentials(String username, String password, boolean secure)
public Optional<NewCookie[]> checkLoginCredentials(String username, String password, boolean secure)
{
return formAuthenticator.isValidCredential(username, password, secure)
.map(user -> createAuthenticationCookie(user, secure));
}

private Optional<String> getAuthenticatedUsername(ContainerRequestContext request)
{
Cookie cookie = request.getCookies().get(TRINO_UI_COOKIE);
if (cookie == null) {
return Optional.empty();
}

try {
return Optional.of(parseJwt(cookie.getValue()));
return MULTIPART_COOKIE.read(request.getCookies()).map(this::parseJwt);
}
catch (JwtException e) {
return Optional.empty();
Expand All @@ -249,35 +247,14 @@ private Optional<String> getAuthenticatedUsername(ContainerRequestContext reques
}
}

private NewCookie createAuthenticationCookie(String userName, boolean secure)
private NewCookie[] createAuthenticationCookie(String userName, boolean secure)
{
String jwt = jwtGenerator.apply(userName);
return new NewCookie(
TRINO_UI_COOKIE,
jwt,
"/ui",
null,
Cookie.DEFAULT_VERSION,
null,
NewCookie.DEFAULT_MAX_AGE,
null,
secure,
true);
return MULTIPART_COOKIE.create(jwtGenerator.apply(userName), null, secure);
}

public static NewCookie getDeleteCookie(boolean secure)
public static NewCookie[] getDeleteCookies(Map<String, Cookie> existingCookies, boolean isSecure)
{
return new NewCookie(
TRINO_UI_COOKIE,
"delete",
"/ui",
null,
Cookie.DEFAULT_VERSION,
null,
0,
null,
secure,
true);
return MULTIPART_COOKIE.delete(existingCookies, isSecure);
}

public boolean isPasswordAllowed(boolean secure)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import static io.trino.server.ui.FormWebUiAuthenticationFilter.LOGIN_FORM_URI;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_LOGIN;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_LOGOUT;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.getDeleteCookie;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.getDeleteCookies;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.redirectFromSuccessfulLoginResponse;
import static jakarta.ws.rs.core.MediaType.TEXT_HTML;
import static java.nio.charset.StandardCharsets.UTF_8;
Expand Down Expand Up @@ -89,7 +89,7 @@ public Response login(
return Response.seeOther(DISABLED_LOCATION_URI).build();
}

Optional<NewCookie> authenticationCookie = formWebUiAuthenticationManager.checkLoginCredentials(username, password, securityContext.isSecure());
Optional<NewCookie[]> authenticationCookie = formWebUiAuthenticationManager.checkLoginCredentials(username, password, securityContext.isSecure());
if (authenticationCookie.isEmpty()) {
// authentication failed, redirect back to the login page
return Response.seeOther(LOGIN_FORM_URI).build();
Expand All @@ -113,7 +113,7 @@ public Response logout(@Context HttpHeaders httpHeaders, @Context UriInfo uriInf
redirectLocation = DISABLED_LOCATION_URI;
}
return Response.seeOther(redirectLocation)
.cookie(getDeleteCookie(securityContext.isSecure()))
.cookie(getDeleteCookies(httpHeaders.getCookies(), securityContext.isSecure()))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.server.ui;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.NewCookie;

import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

import static com.google.common.base.Strings.isNullOrEmpty;
import static jakarta.ws.rs.core.NewCookie.DEFAULT_MAX_AGE;
import static java.util.Objects.requireNonNull;

public class MultipartUiCookie
{
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis/#section-5.6-3.4.1
public static final int MAXIMUM_COOKIE_SIZE = 4096;

private final String cookieName;
private final Pattern cookiePattern;
private final String location;

public MultipartUiCookie(String cookieName, String location)
{
this.cookieName = requireNonNull(cookieName, "cookieName is null");
this.cookiePattern = Pattern.compile("^" + Pattern.quote(cookieName) + "(_\\d+)?$");
this.location = requireNonNull(location, "location is null");
}

public NewCookie[] create(String token, Instant tokenExpiration, boolean isSecure)
{
Date expiration = Optional.ofNullable(tokenExpiration).map(Date::from).orElse(null);
ImmutableList.Builder<NewCookie> cookiesToSet = ImmutableList.builder();
int index = 0;
for (String part : splitValueByLength(token)) {
cookiesToSet.add(new NewCookie.Builder(cookieName(index++))
.value(part)
.path(location)
.domain(null)
.comment(null)
.maxAge(DEFAULT_MAX_AGE)
.expiry(expiration)
.secure(isSecure)
.httpOnly(true)
.build());
}
return cookiesToSet.build().toArray(new NewCookie[0]);
}

public Optional<String> read(Map<String, Cookie> existingCookies)
{
long cookiesCount = existingCookies.values().stream()
.filter(this::matchesName)
.filter(cookie -> !isNullOrEmpty(cookie.getValue()))
.count();

if (cookiesCount == 0) {
return Optional.empty();
}

StringBuilder token = new StringBuilder();
for (int i = 0; i < cookiesCount; i++) {
Cookie cookie = existingCookies.get(cookieName(i));
if (cookie == null || isNullOrEmpty(cookie.getValue())) {
return Optional.empty(); // non continuous
}
token.append(cookie.getValue());
}
return Optional.of(token.toString());
}

public NewCookie[] delete(Map<String, Cookie> existingCookies, boolean isSecured)
{
ImmutableSet.Builder<NewCookie> cookiesToDelete = ImmutableSet.builder();
cookiesToDelete.add(deleteCookie(cookieName, isSecured)); // Always invalidate first cookie even if it doesn't exist
for (Cookie existingCookie : existingCookies.values()) {
if (matchesName(existingCookie)) {
cookiesToDelete.add(deleteCookie(existingCookie.getName(), isSecured));
}
}
return cookiesToDelete.build().toArray(new NewCookie[0]);
}

private List<String> splitValueByLength(String value)
{
return Splitter.fixedLength(maximumCookieValueLength()).splitToList(value);
}

private boolean matchesName(Cookie cookie)
{
return cookiePattern.matcher(cookie.getName()).matches();
}

@VisibleForTesting
String cookieName(int index)
{
if (index == 0) {
return cookieName;
}

return cookieName + '_' + index;
}

int maximumCookieValueLength()
{
// A browser should be able to accept at least 300 cookies with a maximum size of 4096 bytes, as stipulated by RFC 2109 (#6.3), RFC 2965 (#5.3), and RFC 6265
return MAXIMUM_COOKIE_SIZE - cookieName(999).length();
}

private NewCookie deleteCookie(String name, boolean isSecured)
{
return new NewCookie.Builder(name)
.value("delete")
.path(location)
.domain(null)
.maxAge(0)
.expiry(null)
.secure(isSecured)
.httpOnly(true)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@
import static io.trino.server.ui.FormWebUiAuthenticationFilter.DISABLED_LOCATION;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.DISABLED_LOCATION_URI;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.TRINO_FORM_LOGIN;
import static io.trino.server.ui.OAuthIdTokenCookie.ID_TOKEN_COOKIE;
import static io.trino.server.ui.OAuthWebUiCookie.OAUTH2_COOKIE;
import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;
import static java.util.Objects.requireNonNull;

Expand Down Expand Up @@ -125,7 +123,7 @@ public void filter(ContainerRequestContext request)
private Optional<TokenPair> getTokenPair(ContainerRequestContext request)
{
try {
return OAuthWebUiCookie.read(request.getCookies().get(OAUTH2_COOKIE))
return OAuthWebUiCookie.read(request.getCookies())
.map(tokenPairSerializer::deserialize);
}
catch (Exception e) {
Expand Down Expand Up @@ -168,7 +166,7 @@ private void redirectForNewToken(ContainerRequestContext request, String refresh
Response.ResponseBuilder builder = Response.temporaryRedirect(request.getUriInfo().getRequestUri())
.cookie(OAuthWebUiCookie.create(serializedToken, newExpirationTime));

OAuthIdTokenCookie.read(request.getCookies().get(ID_TOKEN_COOKIE))
OAuthIdTokenCookie.read(request.getCookies())
.ifPresent(idToken -> builder.cookie(OAuthIdTokenCookie.create(idToken, newExpirationTime)));

request.abortWith(builder.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import static io.trino.server.security.ResourceSecurity.AccessType.PUBLIC;
import static io.trino.server.security.ResourceSecurity.AccessType.WEB_UI;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_LOGOUT;
import static io.trino.server.ui.OAuthIdTokenCookie.ID_TOKEN_COOKIE;
import static io.trino.server.ui.OAuthWebUiCookie.delete;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
Expand All @@ -54,13 +53,14 @@ public OAuth2WebUiLogoutResource(OAuth2Client auth2Client)
public Response logout(@Context HttpHeaders httpHeaders, @Context UriInfo uriInfo, @Context SecurityContext securityContext)
throws IOException
{
Optional<String> idToken = OAuthIdTokenCookie.read(httpHeaders.getCookies().get(ID_TOKEN_COOKIE));
Optional<String> idToken = OAuthIdTokenCookie.read(httpHeaders.getCookies());
URI callBackUri = UriBuilder.fromUri(uriInfo.getAbsolutePath())
.path("logout.html")
.build();

return Response.seeOther(auth2Client.getLogoutEndpoint(idToken, callBackUri).orElse(callBackUri))
.cookie(delete(), OAuthIdTokenCookie.delete())
.cookie(OAuthIdTokenCookie.delete(httpHeaders.getCookies()))
.cookie(delete(httpHeaders.getCookies()))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,31 @@
import jakarta.ws.rs.core.NewCookie;

import java.time.Instant;
import java.util.Date;
import java.util.Map;
import java.util.Optional;

import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_LOCATION;
import static jakarta.ws.rs.core.Cookie.DEFAULT_VERSION;
import static jakarta.ws.rs.core.NewCookie.DEFAULT_MAX_AGE;
import static java.util.function.Predicate.not;

public final class OAuthIdTokenCookie
{
// prefix according to: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05#section-4.1.3.1
public static final String ID_TOKEN_COOKIE = "__Secure-Trino-ID-Token";
private static final MultipartUiCookie MULTIPART_COOKIE = new MultipartUiCookie(ID_TOKEN_COOKIE, UI_LOCATION);

private OAuthIdTokenCookie() {}

public static NewCookie create(String token, Instant tokenExpiration)
public static NewCookie[] create(String token, Instant tokenExpiration)
{
return new NewCookie(
ID_TOKEN_COOKIE,
token,
UI_LOCATION,
null,
DEFAULT_VERSION,
null,
DEFAULT_MAX_AGE,
Date.from(tokenExpiration),
true,
true);
return MULTIPART_COOKIE.create(token, tokenExpiration, true);
}

public static Optional<String> read(Cookie cookie)
public static Optional<String> read(Map<String, Cookie> availableCookies)
{
return Optional.ofNullable(cookie)
.map(Cookie::getValue)
.filter(not(String::isBlank));
return MULTIPART_COOKIE.read(availableCookies);
}

public static NewCookie delete()
public static NewCookie[] delete(Map<String, Cookie> availableCookies)
{
return new NewCookie(
ID_TOKEN_COOKIE,
"delete",
UI_LOCATION,
null,
DEFAULT_VERSION,
null,
0,
null,
true,
true);
return MULTIPART_COOKIE.delete(availableCookies, true);
}
}
Loading