Merge remote-tracking branch 'origin/jetty-12.1.x' into jetty-12.1.x-servletUpgrade
This commit is contained in:
commit
677de2fe17
|
@ -1,445 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.openid;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.statistic.CounterStatistic;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenIdProvider extends ContainerLifeCycle
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class);
|
||||
|
||||
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
|
||||
private static final String AUTH_PATH = "/auth";
|
||||
private static final String TOKEN_PATH = "/token";
|
||||
private static final String END_SESSION_PATH = "/end_session";
|
||||
private final Map<String, User> issuedAuthCodes = new HashMap<>();
|
||||
|
||||
protected final String clientId;
|
||||
protected final String clientSecret;
|
||||
protected final List<String> redirectUris = new ArrayList<>();
|
||||
private final ServerConnector connector;
|
||||
private final Server server;
|
||||
private int port = 0;
|
||||
private String provider;
|
||||
private User preAuthedUser;
|
||||
private final CounterStatistic loggedInUsers = new CounterStatistic();
|
||||
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
String clientId = "CLIENT_ID123";
|
||||
String clientSecret = "PASSWORD123";
|
||||
int port = 5771;
|
||||
String redirectUri = "http://localhost:8080/j_security_check";
|
||||
|
||||
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
|
||||
openIdProvider.addRedirectUri(redirectUri);
|
||||
openIdProvider.setPort(port);
|
||||
openIdProvider.start();
|
||||
try
|
||||
{
|
||||
openIdProvider.join();
|
||||
}
|
||||
finally
|
||||
{
|
||||
openIdProvider.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public OpenIdProvider(String clientId, String clientSecret)
|
||||
{
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
ContextHandlerCollection contexts = new ContextHandlerCollection();
|
||||
contexts.addHandler(new ConfigServlet(CONFIG_PATH));
|
||||
contexts.addHandler(new AuthEndpoint(AUTH_PATH));
|
||||
contexts.addHandler(new TokenEndpoint(TOKEN_PATH));
|
||||
contexts.addHandler(new EndSessionEndpoint(END_SESSION_PATH));
|
||||
server.setHandler(contexts);
|
||||
|
||||
addBean(server);
|
||||
}
|
||||
|
||||
public void setIdTokenDuration(long duration)
|
||||
{
|
||||
_idTokenDuration = duration;
|
||||
}
|
||||
|
||||
public long getIdTokenDuration()
|
||||
{
|
||||
return _idTokenDuration;
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException
|
||||
{
|
||||
server.join();
|
||||
}
|
||||
|
||||
public OpenIdConfiguration getOpenIdConfiguration()
|
||||
{
|
||||
String provider = getProvider();
|
||||
String authEndpoint = provider + AUTH_PATH;
|
||||
String tokenEndpoint = provider + TOKEN_PATH;
|
||||
return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
|
||||
}
|
||||
|
||||
public CounterStatistic getLoggedInUsers()
|
||||
{
|
||||
return loggedInUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
connector.setPort(port);
|
||||
super.doStart();
|
||||
provider = "http://localhost:" + connector.getLocalPort();
|
||||
}
|
||||
|
||||
public void setPort(int port)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void setUser(User user)
|
||||
{
|
||||
this.preAuthedUser = user;
|
||||
}
|
||||
|
||||
public String getProvider()
|
||||
{
|
||||
if (!isStarted() && port == 0)
|
||||
throw new IllegalStateException("Port of OpenIdProvider not configured");
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void addRedirectUri(String uri)
|
||||
{
|
||||
redirectUris.add(uri);
|
||||
}
|
||||
|
||||
public class AuthEndpoint extends ContextHandler
|
||||
{
|
||||
public AuthEndpoint(String contextPath)
|
||||
{
|
||||
super(contextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
switch (request.getMethod())
|
||||
{
|
||||
case "GET":
|
||||
doGet(request, response, callback);
|
||||
break;
|
||||
case "POST":
|
||||
doPost(request, response, callback);
|
||||
break;
|
||||
default:
|
||||
throw new BadMessageException("Unsupported HTTP Method");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void doGet(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
Fields parameters = Request.getParameters(request);
|
||||
if (!clientId.equals(parameters.getValue("client_id")))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid client_id");
|
||||
return;
|
||||
}
|
||||
|
||||
String redirectUri = parameters.getValue("redirect_uri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String scopeString = parameters.getValue("scope");
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
|
||||
if (!scopes.contains("openid"))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no openid scope");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"code".equals(parameters.getValue("response_type")))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "response_type must be code");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = parameters.getValue("state");
|
||||
if (state == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
if (preAuthedUser == null)
|
||||
{
|
||||
response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html");
|
||||
|
||||
String content =
|
||||
"<h2>Login to OpenID Connect Provider</h2>" +
|
||||
"<form action=\"" + AUTH_PATH + "\" method=\"post\">" +
|
||||
"<input type=\"text\" autocomplete=\"off\" placeholder=\"Username\" name=\"username\" required>" +
|
||||
"<input type=\"hidden\" name=\"redirectUri\" value=\"" + redirectUri + "\">" +
|
||||
"<input type=\"hidden\" name=\"state\" value=\"" + state + "\">" +
|
||||
"<input type=\"submit\">" +
|
||||
"</form>";
|
||||
response.write(true, BufferUtil.toBuffer(content), callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
redirectUser(request, response, callback, preAuthedUser, redirectUri, state);
|
||||
}
|
||||
}
|
||||
|
||||
protected void doPost(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
Fields parameters = Request.getParameters(request);
|
||||
|
||||
String redirectUri = parameters.getValue("redirectUri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = parameters.getValue("state");
|
||||
if (state == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
String username = parameters.getValue("username");
|
||||
if (username == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no username");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = new User(username);
|
||||
redirectUser(request, response, callback, user, redirectUri, state);
|
||||
}
|
||||
|
||||
public void redirectUser(Request request, Response response, Callback callback, User user, String redirectUri, String state) throws IOException
|
||||
{
|
||||
String authCode = UUID.randomUUID().toString().replace("-", "");
|
||||
issuedAuthCodes.put(authCode, user);
|
||||
|
||||
try
|
||||
{
|
||||
redirectUri += "?code=" + authCode + "&state=" + state;
|
||||
Response.sendRedirect(request, response, callback, redirectUri);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
issuedAuthCodes.remove(authCode);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenEndpoint extends ContextHandler
|
||||
{
|
||||
public TokenEndpoint(String contextPath)
|
||||
{
|
||||
super(contextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
Fields parameters = Request.getParameters(request);
|
||||
|
||||
String code = parameters.getValue("code");
|
||||
|
||||
if (!clientId.equals(parameters.getValue("client_id")) ||
|
||||
!clientSecret.equals(parameters.getValue("client_secret")) ||
|
||||
!redirectUris.contains(parameters.getValue("redirect_uri")) ||
|
||||
!"authorization_code".equals(parameters.getValue("grant_type")) ||
|
||||
code == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "bad auth request");
|
||||
return true;
|
||||
}
|
||||
|
||||
User user = issuedAuthCodes.remove(code);
|
||||
if (user == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid auth code");
|
||||
return true;
|
||||
}
|
||||
|
||||
String accessToken = "ABCDEFG";
|
||||
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
|
||||
String content = "{" +
|
||||
"\"access_token\": \"" + accessToken + "\"," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
|
||||
"\"expires_in\": " + accessTokenDuration + "," +
|
||||
"\"token_type\": \"Bearer\"" +
|
||||
"}";
|
||||
|
||||
loggedInUsers.increment();
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
|
||||
response.write(true, BufferUtil.toBuffer(content), callback);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class EndSessionEndpoint extends ContextHandler
|
||||
{
|
||||
public EndSessionEndpoint(String contextPath)
|
||||
{
|
||||
super(contextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
Fields parameters = Request.getParameters(request);
|
||||
|
||||
String idToken = parameters.getValue("id_token_hint");
|
||||
if (idToken == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "no id_token_hint");
|
||||
return true;
|
||||
}
|
||||
|
||||
String logoutRedirect = parameters.getValue("post_logout_redirect_uri");
|
||||
if (logoutRedirect == null)
|
||||
{
|
||||
response.setStatus(HttpStatus.OK_200);
|
||||
response.write(true, BufferUtil.toBuffer("logout success on end_session_endpoint"), callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
loggedInUsers.decrement();
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
|
||||
Response.sendRedirect(request, response, callback, logoutRedirect);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigServlet extends ContextHandler
|
||||
{
|
||||
public ConfigServlet(String contextPath)
|
||||
{
|
||||
super(contextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
String discoveryDocument = "{" +
|
||||
"\"issuer\": \"" + provider + "\"," +
|
||||
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
|
||||
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
|
||||
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
|
||||
"}";
|
||||
|
||||
response.write(true, BufferUtil.toBuffer(discoveryDocument), callback);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class User
|
||||
{
|
||||
private final String subject;
|
||||
private final String name;
|
||||
|
||||
public User(String name)
|
||||
{
|
||||
this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name);
|
||||
}
|
||||
|
||||
public User(String subject, String name)
|
||||
{
|
||||
this.subject = subject;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getSubject()
|
||||
{
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getIdToken(String provider, String clientId, long duration)
|
||||
{
|
||||
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (!(obj instanceof User))
|
||||
return false;
|
||||
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(subject, name);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,34 +13,55 @@
|
|||
|
||||
package org.eclipse.jetty.server;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
|
||||
/**
|
||||
* Common components made available via a {@link Request}
|
||||
* Common components made available via a {@link Request}.
|
||||
*/
|
||||
public interface Components
|
||||
{
|
||||
/**
|
||||
* @return the {@link ByteBufferPool} associated with the {@link Request}
|
||||
*/
|
||||
ByteBufferPool getByteBufferPool();
|
||||
|
||||
/**
|
||||
* @return the {@link Scheduler} associated with the {@link Request}
|
||||
*/
|
||||
Scheduler getScheduler();
|
||||
|
||||
/**
|
||||
* @return the {@link ThreadPool} associated with the {@link Request}
|
||||
* @deprecated use {@link #getExecutor()} instead
|
||||
*/
|
||||
@Deprecated(since = "12.0.13", forRemoval = true)
|
||||
ThreadPool getThreadPool();
|
||||
|
||||
/**
|
||||
* A Map which can be used as a cache for object (e.g. Cookie cache).
|
||||
* The cache will have a life cycle limited by the connection, i.e. no cache map will live
|
||||
* @return the {@link Executor} associated with the {@link Request}
|
||||
*/
|
||||
default Executor getExecutor()
|
||||
{
|
||||
return getThreadPool();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>A map-like object that can be used as a cache (for example, as a cookie cache).</p>
|
||||
* <p>The cache will have a life cycle limited by the connection, i.e. no cache map will live
|
||||
* longer that the connection associated with it. However, a cache may have a shorter life
|
||||
* than a connection (e.g. it may be discarded for implementation reasons). A cache map is
|
||||
* guaranteed to be given to only a single request concurrently (scoped by
|
||||
* {@link org.eclipse.jetty.server.internal.HttpChannelState}), so objects saved there do not
|
||||
* need to be made safe from access by simultaneous request.
|
||||
* If the connection is known to be none-persistent then the cache may be a noop
|
||||
* cache and discard all items set on it.
|
||||
* If the connection is known to be non-persistent then the cache may be a noop
|
||||
* cache and discard all items set on it.</p>
|
||||
*
|
||||
* @return A Map, which may be an empty map that discards all items.
|
||||
* @return A map-like object, which may be an empty implementation that discards all items.
|
||||
*/
|
||||
Attributes getCache();
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import org.eclipse.jetty.http.MultiPartCompliance;
|
|||
import org.eclipse.jetty.http.MultiPartConfig;
|
||||
import org.eclipse.jetty.http.Trailers;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.server.internal.CompletionStreamWrapper;
|
||||
import org.eclipse.jetty.server.internal.HttpChannelState;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
|
@ -716,6 +717,7 @@ public interface Request extends Attributes, Content.Source
|
|||
* is returned, then this method must not generate a response, nor complete the callback.
|
||||
* @throws Exception if there is a failure during the handling. Catchers cannot assume that the callback will be
|
||||
* called and thus should attempt to complete the request as if a false had been returned.
|
||||
* @see AbortException
|
||||
*/
|
||||
boolean handle(Request request, Response response, Callback callback) throws Exception;
|
||||
|
||||
|
@ -725,6 +727,34 @@ public interface Request extends Attributes, Content.Source
|
|||
{
|
||||
return InvocationType.BLOCKING;
|
||||
}
|
||||
|
||||
/**
|
||||
* A marker {@link Exception} that can be passed the {@link Callback#failed(Throwable)} of the {@link Callback}
|
||||
* passed in {@link #handle(Request, Response, Callback)}, to cause request handling to be aborted. For HTTP/1
|
||||
* an abort is handled with a {@link EndPoint#close()}, for later versions of HTTP, a reset message will be sent.
|
||||
*/
|
||||
class AbortException extends Exception
|
||||
{
|
||||
public AbortException()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
public AbortException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AbortException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public AbortException(Throwable cause)
|
||||
{
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -397,13 +397,18 @@ public class QoSHandler extends ConditionalHandler.Abstract
|
|||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} resuming {}", this, entry.request);
|
||||
// Always dispatch to avoid StackOverflowError.
|
||||
getServer().getThreadPool().execute(entry);
|
||||
execute(entry.request, entry);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void execute(Request request, Runnable task)
|
||||
{
|
||||
request.getComponents().getExecutor().execute(task);
|
||||
}
|
||||
|
||||
private class Entry implements CyclicTimeouts.Expirable, Runnable
|
||||
{
|
||||
private final Request request;
|
||||
|
@ -458,7 +463,7 @@ public class QoSHandler extends ConditionalHandler.Abstract
|
|||
}
|
||||
|
||||
if (removed)
|
||||
failSuspended(request, response, callback, HttpStatus.SERVICE_UNAVAILABLE_503, new TimeoutException());
|
||||
execute(request, () -> failSuspended(request, response, callback, HttpStatus.SERVICE_UNAVAILABLE_503, new TimeoutException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -63,6 +64,7 @@ import org.eclipse.jetty.util.BufferUtil;
|
|||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.ExceptionUtil;
|
||||
import org.eclipse.jetty.util.NanoTime;
|
||||
import org.eclipse.jetty.util.VirtualThreads;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.eclipse.jetty.util.thread.Invocable;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
|
@ -231,7 +233,18 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
@Override
|
||||
public ThreadPool getThreadPool()
|
||||
{
|
||||
return getServer().getThreadPool();
|
||||
Executor executor = getExecutor();
|
||||
if (executor instanceof ThreadPool threadPool)
|
||||
return threadPool;
|
||||
return new ThreadPoolWrapper(executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Executor getExecutor()
|
||||
{
|
||||
Executor executor = getServer().getThreadPool();
|
||||
Executor virtualExecutor = VirtualThreads.getVirtualThreadsExecutor(executor);
|
||||
return virtualExecutor != null ? virtualExecutor : executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1577,6 +1590,8 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
|
||||
httpChannelState._callbackFailure = failure;
|
||||
|
||||
if (!stream.isCommitted() && !(failure instanceof Request.Handler.AbortException))
|
||||
{
|
||||
// Consume any input.
|
||||
Throwable unconsumed = stream.consumeAvailable();
|
||||
ExceptionUtil.addSuppressedIfNotAssociated(failure, unconsumed);
|
||||
|
@ -1585,11 +1600,9 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed stream.isCommitted={}, response.isCommitted={} {}", stream.isCommitted(), response.isCommitted(), this);
|
||||
|
||||
// There may have been an attempt to write an error response that failed.
|
||||
// Do not try to write again an error response if already committed.
|
||||
if (!stream.isCommitted())
|
||||
errorResponse = new ErrorResponse(request);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorResponse != null)
|
||||
Response.writeError(request, errorResponse, new ErrorCallback(request, errorResponse, stream, failure), failure);
|
||||
|
@ -1948,4 +1961,43 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThreadPoolWrapper implements ThreadPool
|
||||
{
|
||||
private final Executor _executor;
|
||||
|
||||
private ThreadPoolWrapper(Executor executor)
|
||||
{
|
||||
_executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command)
|
||||
{
|
||||
_executor.execute(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void join()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getThreads()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIdleThreads()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLowOnThreads()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ public class ServerTest
|
|||
{
|
||||
Runnable after = _afterHandle.getAndSet(null);
|
||||
if (after != null)
|
||||
getThreadPool().execute(after);
|
||||
getExecutor().execute(after);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,6 +13,9 @@
|
|||
|
||||
package org.eclipse.jetty.server.handler;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -28,11 +31,15 @@ import org.eclipse.jetty.server.LocalConnector;
|
|||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.VirtualThreads;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
|
@ -51,6 +58,7 @@ public class QoSHandlerTest
|
|||
|
||||
private void start(QoSHandler qosHandler) throws Exception
|
||||
{
|
||||
if (server == null)
|
||||
server = new Server();
|
||||
connector = new LocalConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
@ -76,7 +84,6 @@ public class QoSHandlerTest
|
|||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO fix in #12171
|
||||
public void testRequestIsSuspendedAndResumed() throws Exception
|
||||
{
|
||||
int maxRequests = 2;
|
||||
|
@ -144,7 +151,6 @@ public class QoSHandlerTest
|
|||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO fix in #12171
|
||||
public void testSuspendedRequestTimesOut() throws Exception
|
||||
{
|
||||
int maxRequests = 1;
|
||||
|
@ -486,4 +492,70 @@ public class QoSHandlerTest
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledForJreRange(max = JRE.JAVA_20)
|
||||
public void testRequestInVirtualThreadIsResumedInVirtualThread() throws Exception
|
||||
{
|
||||
QoSHandler qosHandler = new QoSHandler();
|
||||
qosHandler.setMaxRequestCount(1);
|
||||
List<Callback> callbacks = new ArrayList<>();
|
||||
qosHandler.setHandler(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback)
|
||||
{
|
||||
response.setStatus(VirtualThreads.isVirtualThread() ? HttpStatus.OK_200 : HttpStatus.NOT_ACCEPTABLE_406);
|
||||
// Save the callback but do not succeed it yet.
|
||||
callbacks.add(callback);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
QueuedThreadPool serverThreads = new QueuedThreadPool();
|
||||
serverThreads.setName("st");
|
||||
serverThreads.setVirtualThreadsExecutor(VirtualThreads.getNamedVirtualThreadsExecutor("vst"));
|
||||
server = new Server(serverThreads);
|
||||
ServerConnector networkConnector = new ServerConnector(server, 1, 1);
|
||||
server.addConnector(networkConnector);
|
||||
start(qosHandler);
|
||||
|
||||
// Send the first request that will not be completed yet.
|
||||
try (SocketChannel client1 = SocketChannel.open(new InetSocketAddress("localhost", networkConnector.getLocalPort())))
|
||||
{
|
||||
client1.write(StandardCharsets.UTF_8.encode("""
|
||||
GET /first HTTP/1.1
|
||||
Host: localhost
|
||||
|
||||
"""));
|
||||
// Wait that the request arrives at the server.
|
||||
await().atMost(5, TimeUnit.SECONDS).until(callbacks::size, is(1));
|
||||
|
||||
// Send the second request, it should be suspended by QoSHandler.
|
||||
try (SocketChannel client2 = SocketChannel.open(new InetSocketAddress("localhost", networkConnector.getLocalPort())))
|
||||
{
|
||||
client2.write(StandardCharsets.UTF_8.encode("""
|
||||
GET /second HTTP/1.1
|
||||
Host: localhost
|
||||
|
||||
"""));
|
||||
// Wait for the second request to be suspended.
|
||||
await().atMost(5, TimeUnit.SECONDS).until(qosHandler::getSuspendedRequestCount, is(1));
|
||||
|
||||
// Finish the first request, so that the second can be resumed.
|
||||
callbacks.remove(0).succeeded();
|
||||
client1.socket().setSoTimeout(5000);
|
||||
HttpTester.Response response1 = HttpTester.parseResponse(client1);
|
||||
assertEquals(HttpStatus.OK_200, response1.getStatus());
|
||||
|
||||
// Wait for the second request to arrive to the server.
|
||||
await().atMost(5, TimeUnit.SECONDS).until(callbacks::size, is(1));
|
||||
|
||||
// Finish the second request.
|
||||
callbacks.remove(0).succeeded();
|
||||
client2.socket().setSoTimeout(5000);
|
||||
HttpTester.Response response2 = HttpTester.parseResponse(client2);
|
||||
assertEquals(HttpStatus.OK_200, response2.getStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -816,4 +816,25 @@ public class TypeUtil
|
|||
int result = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(value - 1));
|
||||
return result > 0 ? result : Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is a method has been declared on the class of an instance
|
||||
* @param object The object to check
|
||||
* @param methodName The method name
|
||||
* @param args The arguments to the method
|
||||
* @return {@code true} iff {@link Class#getDeclaredMethod(String, Class[])} can be called on the
|
||||
* {@link Class} of the object, without throwing {@link NoSuchMethodException}.
|
||||
*/
|
||||
public static boolean isDeclaredMethodOn(Object object, String methodName, Class<?>... args)
|
||||
{
|
||||
try
|
||||
{
|
||||
object.getClass().getDeclaredMethod(methodName, args);
|
||||
return true;
|
||||
}
|
||||
catch (NoSuchMethodException e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,6 @@ public class VirtualThreads
|
|||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
warn();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ import javax.net.ssl.X509ExtendedTrustManager;
|
|||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
import org.eclipse.jetty.util.annotation.ManagedAttribute;
|
||||
import org.eclipse.jetty.util.annotation.ManagedObject;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
|
@ -86,6 +87,7 @@ import org.eclipse.jetty.util.resource.ResourceFactory;
|
|||
import org.eclipse.jetty.util.resource.Resources;
|
||||
import org.eclipse.jetty.util.security.CertificateUtils;
|
||||
import org.eclipse.jetty.util.security.CertificateValidator;
|
||||
import org.eclipse.jetty.util.security.Credential;
|
||||
import org.eclipse.jetty.util.security.Password;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -157,9 +159,9 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du
|
|||
private Resource _trustStoreResource;
|
||||
private String _trustStoreProvider;
|
||||
private String _trustStoreType;
|
||||
private Password _keyStorePassword;
|
||||
private Password _keyManagerPassword;
|
||||
private Password _trustStorePassword;
|
||||
private Credential _keyStoreCredential;
|
||||
private Credential _keyManagerCredential;
|
||||
private Credential _trustStoreCredential;
|
||||
private String _sslProvider;
|
||||
private String _sslProtocol = "TLS";
|
||||
private String _secureRandomAlgorithm;
|
||||
|
@ -811,46 +813,42 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du
|
|||
|
||||
public String getKeyStorePassword()
|
||||
{
|
||||
return _keyStorePassword == null ? null : _keyStorePassword.toString();
|
||||
return _keyStoreCredential == null ? null : _keyStoreCredential.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param password The password for the key store. If null is passed and
|
||||
* a keystore is set, then
|
||||
* the {@link #getPassword(String)} is used to
|
||||
* obtain a password either from the {@value #PASSWORD_PROPERTY}
|
||||
* system property.
|
||||
* @param password The password for the key store. If null is passed then
|
||||
* {@link #getCredential(String)} is used to obtain a password from
|
||||
* the {@value #PASSWORD_PROPERTY} system property.
|
||||
*/
|
||||
public void setKeyStorePassword(String password)
|
||||
{
|
||||
_keyStorePassword = password == null ? getPassword(PASSWORD_PROPERTY) : newPassword(password);
|
||||
_keyStoreCredential = password == null ? getCredential(PASSWORD_PROPERTY) : newCredential(password);
|
||||
}
|
||||
|
||||
public String getKeyManagerPassword()
|
||||
{
|
||||
return _keyManagerPassword == null ? null : _keyManagerPassword.toString();
|
||||
return _keyManagerCredential == null ? null : _keyManagerCredential.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param password The password (if any) for the specific key within the key store.
|
||||
* If null is passed and the {@value #KEYPASSWORD_PROPERTY} system property is set,
|
||||
* then the {@link #getPassword(String)} is used to
|
||||
* If null is passed then {@link #getCredential(String)} is used to
|
||||
* obtain a password from the {@value #KEYPASSWORD_PROPERTY} system property.
|
||||
*/
|
||||
public void setKeyManagerPassword(String password)
|
||||
{
|
||||
_keyManagerPassword = password == null ? getPassword(KEYPASSWORD_PROPERTY) : newPassword(password);
|
||||
_keyManagerCredential = password == null ? getCredential(KEYPASSWORD_PROPERTY) : newCredential(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param password The password for the truststore. If null is passed then
|
||||
* the {@link #getPassword(String)} is used to
|
||||
* obtain a password from the {@value #PASSWORD_PROPERTY}
|
||||
* {@link #getCredential(String)} is used to obtain a password from the {@value #PASSWORD_PROPERTY}
|
||||
* system property.
|
||||
*/
|
||||
public void setTrustStorePassword(String password)
|
||||
{
|
||||
_trustStorePassword = password == null ? getPassword(PASSWORD_PROPERTY) : newPassword(password);
|
||||
_trustStoreCredential = password == null ? getCredential(PASSWORD_PROPERTY) : newCredential(password);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1133,7 +1131,7 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du
|
|||
*/
|
||||
protected KeyStore loadKeyStore(Resource resource) throws Exception
|
||||
{
|
||||
String storePassword = Objects.toString(_keyStorePassword, null);
|
||||
String storePassword = Objects.toString(_keyStoreCredential, null);
|
||||
return CertificateUtils.getKeyStore(resource, getKeyStoreType(), getKeyStoreProvider(), storePassword);
|
||||
}
|
||||
|
||||
|
@ -1148,12 +1146,12 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du
|
|||
{
|
||||
String type = Objects.toString(getTrustStoreType(), getKeyStoreType());
|
||||
String provider = Objects.toString(getTrustStoreProvider(), getKeyStoreProvider());
|
||||
Password passwd = _trustStorePassword;
|
||||
Credential passwd = _trustStoreCredential;
|
||||
if (resource == null || resource.equals(_keyStoreResource))
|
||||
{
|
||||
resource = _keyStoreResource;
|
||||
if (passwd == null)
|
||||
passwd = _keyStorePassword;
|
||||
passwd = _keyStoreCredential;
|
||||
}
|
||||
return CertificateUtils.getKeyStore(resource, type, provider, Objects.toString(passwd, null));
|
||||
}
|
||||
|
@ -1180,7 +1178,7 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du
|
|||
if (keyStore != null)
|
||||
{
|
||||
KeyManagerFactory keyManagerFactory = getKeyManagerFactoryInstance();
|
||||
keyManagerFactory.init(keyStore, _keyManagerPassword == null ? (_keyStorePassword == null ? null : _keyStorePassword.toString().toCharArray()) : _keyManagerPassword.toString().toCharArray());
|
||||
keyManagerFactory.init(keyStore, _keyManagerCredential == null ? (_keyStoreCredential == null ? null : _keyStoreCredential.toString().toCharArray()) : _keyManagerCredential.toString().toCharArray());
|
||||
managers = keyManagerFactory.getKeyManagers();
|
||||
|
||||
if (managers != null)
|
||||
|
@ -1615,7 +1613,9 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du
|
|||
*
|
||||
* @param realm the realm
|
||||
* @return the Password object
|
||||
* @deprecated use {#link getCredential} instead.
|
||||
*/
|
||||
@Deprecated(since = "12.0.13", forRemoval = true)
|
||||
protected Password getPassword(String realm)
|
||||
{
|
||||
String password = System.getProperty(realm);
|
||||
|
@ -1627,12 +1627,43 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du
|
|||
*
|
||||
* @param password the password string
|
||||
* @return the new Password object
|
||||
* @deprecated use {#link newCredential} instead.
|
||||
*/
|
||||
@Deprecated(since = "12.0.13", forRemoval = true)
|
||||
public Password newPassword(String password)
|
||||
{
|
||||
return new Password(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the credential object for the given realm.
|
||||
*
|
||||
* @param realm the realm
|
||||
* @return the Credential object
|
||||
*/
|
||||
protected Credential getCredential(String realm)
|
||||
{
|
||||
if (TypeUtil.isDeclaredMethodOn(this, "getPassword", String.class))
|
||||
return getPassword(realm);
|
||||
|
||||
String password = System.getProperty(realm);
|
||||
return password == null ? null : newCredential(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Credential object.
|
||||
*
|
||||
* @param password the password string
|
||||
* @return the new Credential object
|
||||
*/
|
||||
public Credential newCredential(String password)
|
||||
{
|
||||
if (TypeUtil.isDeclaredMethodOn(this, "newPassword", String.class))
|
||||
return newPassword(password);
|
||||
|
||||
return Credential.getCredential(password);
|
||||
}
|
||||
|
||||
public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException
|
||||
{
|
||||
checkIsStarted();
|
||||
|
|
|
@ -267,4 +267,34 @@ public class TypeUtilTest
|
|||
assertThat(TypeUtil.ceilToNextPowerOfTwo(5), is(8));
|
||||
assertThat(TypeUtil.ceilToNextPowerOfTwo(Integer.MAX_VALUE - 1), is(Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
public static class Base
|
||||
{
|
||||
protected String methodA(String arg)
|
||||
{
|
||||
return "a" + arg.length();
|
||||
}
|
||||
|
||||
protected String methodB(String arg)
|
||||
{
|
||||
return "b" + arg.length();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Example extends Base
|
||||
{
|
||||
@Override
|
||||
protected String methodB(String arg)
|
||||
{
|
||||
return "B" + arg;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsMethodDeclaredOn()
|
||||
{
|
||||
Example example = new Example();
|
||||
assertFalse(TypeUtil.isDeclaredMethodOn(example, "methodA", String.class));
|
||||
assertTrue(TypeUtil.isDeclaredMethodOn(example, "methodB", String.class));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<module>jetty-jndi</module>
|
||||
<module>jetty-keystore</module>
|
||||
<module>jetty-maven</module>
|
||||
<module>jetty-openid</module>
|
||||
<module>jetty-osgi</module>
|
||||
<module>jetty-plus</module>
|
||||
<module>jetty-proxy</module>
|
||||
|
|
|
@ -140,7 +140,7 @@ public class ServletApiResponse implements HttpServletResponse
|
|||
{
|
||||
switch (sc)
|
||||
{
|
||||
case -1 -> getServletChannel().abort(new IOException(msg));
|
||||
case -1 -> getServletChannel().abort(new Request.Handler.AbortException(msg));
|
||||
case HttpStatus.PROCESSING_102, HttpStatus.EARLY_HINTS_103 ->
|
||||
{
|
||||
if (!isCommitted())
|
||||
|
|
|
@ -13,8 +13,12 @@
|
|||
|
||||
package org.eclipse.jetty.ee10.servlet;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
@ -50,6 +54,7 @@ import org.eclipse.jetty.server.LocalConnector;
|
|||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
|
@ -70,6 +75,7 @@ import static org.hamcrest.Matchers.is;
|
|||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -780,6 +786,220 @@ public class ErrorPageTest
|
|||
assertThat(responseBody, Matchers.containsString("ERROR_REQUEST_URI: /fail/599"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortWithSendError() throws Exception
|
||||
{
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
HttpServlet failServlet = new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
response.sendError(-1);
|
||||
}
|
||||
};
|
||||
|
||||
contextHandler.addServlet(failServlet, "/abort");
|
||||
startServer(contextHandler);
|
||||
|
||||
ServerConnector connector = new ServerConnector(_server);
|
||||
connector.setPort(0);
|
||||
_server.addConnector(connector);
|
||||
connector.start();
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String request = """
|
||||
GET /abort HTTP/1.1\r
|
||||
Host: test\r
|
||||
\r
|
||||
""";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String line = in.readLine();
|
||||
assertNull(line);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortWithSendErrorChunked() throws Exception
|
||||
{
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
HttpServlet failServlet = new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
response.getOutputStream().write("test".getBytes(StandardCharsets.UTF_8));
|
||||
response.flushBuffer();
|
||||
response.sendError(-1);
|
||||
}
|
||||
};
|
||||
|
||||
contextHandler.addServlet(failServlet, "/abort");
|
||||
startServer(contextHandler);
|
||||
|
||||
ServerConnector connector = new ServerConnector(_server);
|
||||
connector.setPort(0);
|
||||
_server.addConnector(connector);
|
||||
connector.start();
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String request = """
|
||||
GET /abort HTTP/1.1\r
|
||||
Host: test\r
|
||||
\r
|
||||
""";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String line = in.readLine();
|
||||
assertThat(line, is("HTTP/1.1 200 OK"));
|
||||
|
||||
boolean chunked = false;
|
||||
while (!line.isEmpty())
|
||||
{
|
||||
line = in.readLine();
|
||||
assertNotNull(line);
|
||||
chunked |= line.equals("Transfer-Encoding: chunked");
|
||||
}
|
||||
assertTrue(chunked);
|
||||
|
||||
line = in.readLine();
|
||||
assertThat(line, is("4"));
|
||||
line = in.readLine();
|
||||
assertThat(line, is("test"));
|
||||
|
||||
line = in.readLine();
|
||||
assertNull(line);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortWithSendErrorContent() throws Exception
|
||||
{
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
HttpServlet failServlet = new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
response.setContentLength(10);
|
||||
response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
response.flushBuffer();
|
||||
response.sendError(-1);
|
||||
}
|
||||
};
|
||||
|
||||
contextHandler.addServlet(failServlet, "/abort");
|
||||
startServer(contextHandler);
|
||||
|
||||
ServerConnector connector = new ServerConnector(_server);
|
||||
connector.setPort(0);
|
||||
_server.addConnector(connector);
|
||||
connector.start();
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String request = """
|
||||
GET /abort HTTP/1.1\r
|
||||
Host: test\r
|
||||
\r
|
||||
""";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String line = in.readLine();
|
||||
assertThat(line, is("HTTP/1.1 200 OK"));
|
||||
|
||||
boolean chunked = false;
|
||||
while (!line.isEmpty())
|
||||
{
|
||||
line = in.readLine();
|
||||
assertNotNull(line);
|
||||
chunked |= line.equals("Transfer-Encoding: chunked");
|
||||
}
|
||||
assertFalse(chunked);
|
||||
|
||||
line = in.readLine();
|
||||
assertThat(line, is("test"));
|
||||
|
||||
line = in.readLine();
|
||||
assertNull(line);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortWithSendErrorComplete() throws Exception
|
||||
{
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
HttpServlet failServlet = new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
response.setContentLength(6);
|
||||
response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
response.sendError(-1);
|
||||
}
|
||||
};
|
||||
|
||||
contextHandler.addServlet(failServlet, "/abort");
|
||||
startServer(contextHandler);
|
||||
|
||||
ServerConnector connector = new ServerConnector(_server);
|
||||
connector.setPort(0);
|
||||
_server.addConnector(connector);
|
||||
connector.start();
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String request = """
|
||||
GET /abort HTTP/1.1\r
|
||||
Host: test\r
|
||||
\r
|
||||
""";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String line = in.readLine();
|
||||
assertThat(line, is("HTTP/1.1 200 OK"));
|
||||
|
||||
boolean chunked = false;
|
||||
while (!line.isEmpty())
|
||||
{
|
||||
line = in.readLine();
|
||||
assertNotNull(line);
|
||||
chunked |= line.equals("Transfer-Encoding: chunked");
|
||||
}
|
||||
assertFalse(chunked);
|
||||
|
||||
line = in.readLine();
|
||||
assertThat(line, is("test"));
|
||||
|
||||
line = in.readLine();
|
||||
assertNull(line);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorCodeNoDefaultServletNonExistentErrorLocation() throws Exception
|
||||
{
|
||||
|
|
|
@ -143,7 +143,7 @@ public class ServletApiResponse implements HttpServletResponse
|
|||
{
|
||||
switch (sc)
|
||||
{
|
||||
case -1 -> getServletChannel().abort(new IOException(msg));
|
||||
case -1 -> getServletChannel().abort(new Request.Handler.AbortException(msg));
|
||||
case HttpStatus.PROCESSING_102, HttpStatus.EARLY_HINTS_103 ->
|
||||
{
|
||||
if (!isCommitted())
|
||||
|
|
|
@ -13,8 +13,12 @@
|
|||
|
||||
package org.eclipse.jetty.ee11.servlet;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
@ -50,6 +54,7 @@ import org.eclipse.jetty.server.LocalConnector;
|
|||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
|
@ -70,6 +75,7 @@ import static org.hamcrest.Matchers.is;
|
|||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -836,6 +842,220 @@ public class ErrorPageTest
|
|||
assertThat(responseBody, Matchers.containsString("ERROR_REQUEST_URI: /fail/599"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortWithSendError() throws Exception
|
||||
{
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
HttpServlet failServlet = new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
response.sendError(-1);
|
||||
}
|
||||
};
|
||||
|
||||
contextHandler.addServlet(failServlet, "/abort");
|
||||
startServer(contextHandler);
|
||||
|
||||
ServerConnector connector = new ServerConnector(_server);
|
||||
connector.setPort(0);
|
||||
_server.addConnector(connector);
|
||||
connector.start();
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String request = """
|
||||
GET /abort HTTP/1.1\r
|
||||
Host: test\r
|
||||
\r
|
||||
""";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String line = in.readLine();
|
||||
assertNull(line);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortWithSendErrorChunked() throws Exception
|
||||
{
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
HttpServlet failServlet = new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
response.getOutputStream().write("test".getBytes(StandardCharsets.UTF_8));
|
||||
response.flushBuffer();
|
||||
response.sendError(-1);
|
||||
}
|
||||
};
|
||||
|
||||
contextHandler.addServlet(failServlet, "/abort");
|
||||
startServer(contextHandler);
|
||||
|
||||
ServerConnector connector = new ServerConnector(_server);
|
||||
connector.setPort(0);
|
||||
_server.addConnector(connector);
|
||||
connector.start();
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String request = """
|
||||
GET /abort HTTP/1.1\r
|
||||
Host: test\r
|
||||
\r
|
||||
""";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String line = in.readLine();
|
||||
assertThat(line, is("HTTP/1.1 200 OK"));
|
||||
|
||||
boolean chunked = false;
|
||||
while (!line.isEmpty())
|
||||
{
|
||||
line = in.readLine();
|
||||
assertNotNull(line);
|
||||
chunked |= line.equals("Transfer-Encoding: chunked");
|
||||
}
|
||||
assertTrue(chunked);
|
||||
|
||||
line = in.readLine();
|
||||
assertThat(line, is("4"));
|
||||
line = in.readLine();
|
||||
assertThat(line, is("test"));
|
||||
|
||||
line = in.readLine();
|
||||
assertNull(line);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortWithSendErrorContent() throws Exception
|
||||
{
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
HttpServlet failServlet = new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
response.setContentLength(10);
|
||||
response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
response.flushBuffer();
|
||||
response.sendError(-1);
|
||||
}
|
||||
};
|
||||
|
||||
contextHandler.addServlet(failServlet, "/abort");
|
||||
startServer(contextHandler);
|
||||
|
||||
ServerConnector connector = new ServerConnector(_server);
|
||||
connector.setPort(0);
|
||||
_server.addConnector(connector);
|
||||
connector.start();
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String request = """
|
||||
GET /abort HTTP/1.1\r
|
||||
Host: test\r
|
||||
\r
|
||||
""";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String line = in.readLine();
|
||||
assertThat(line, is("HTTP/1.1 200 OK"));
|
||||
|
||||
boolean chunked = false;
|
||||
while (!line.isEmpty())
|
||||
{
|
||||
line = in.readLine();
|
||||
assertNotNull(line);
|
||||
chunked |= line.equals("Transfer-Encoding: chunked");
|
||||
}
|
||||
assertFalse(chunked);
|
||||
|
||||
line = in.readLine();
|
||||
assertThat(line, is("test"));
|
||||
|
||||
line = in.readLine();
|
||||
assertNull(line);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortWithSendErrorComplete() throws Exception
|
||||
{
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
HttpServlet failServlet = new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
response.setContentLength(6);
|
||||
response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
response.sendError(-1);
|
||||
}
|
||||
};
|
||||
|
||||
contextHandler.addServlet(failServlet, "/abort");
|
||||
startServer(contextHandler);
|
||||
|
||||
ServerConnector connector = new ServerConnector(_server);
|
||||
connector.setPort(0);
|
||||
_server.addConnector(connector);
|
||||
connector.start();
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String request = """
|
||||
GET /abort HTTP/1.1\r
|
||||
Host: test\r
|
||||
\r
|
||||
""";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String line = in.readLine();
|
||||
assertThat(line, is("HTTP/1.1 200 OK"));
|
||||
|
||||
boolean chunked = false;
|
||||
while (!line.isEmpty())
|
||||
{
|
||||
line = in.readLine();
|
||||
assertNotNull(line);
|
||||
chunked |= line.equals("Transfer-Encoding: chunked");
|
||||
}
|
||||
assertFalse(chunked);
|
||||
|
||||
line = in.readLine();
|
||||
assertThat(line, is("test"));
|
||||
|
||||
line = in.readLine();
|
||||
assertNull(line);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorCodeNoDefaultServletNonExistentErrorLocation() throws Exception
|
||||
{
|
||||
|
|
|
@ -52,6 +52,11 @@
|
|||
<artifactId>jetty-ee8-servlet</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-test-common</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
|
|
|
@ -488,7 +488,7 @@ public class Response implements HttpServletResponse
|
|||
|
||||
switch (code)
|
||||
{
|
||||
case -1 -> _channel.abort(new IOException(message));
|
||||
case -1 -> _channel.abort(new org.eclipse.jetty.server.Request.Handler.AbortException(message));
|
||||
case HttpStatus.PROCESSING_102 -> sendProcessing();
|
||||
case HttpStatus.EARLY_HINTS_103 -> sendEarlyHint();
|
||||
default -> _channel.getState().sendError(code, message);
|
||||
|
|
|
@ -55,6 +55,11 @@
|
|||
<artifactId>jetty-ee9-servlet</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-test-common</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.security.openid;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* A basic JWT encoder for testing purposes.
|
||||
*/
|
||||
public class JwtEncoder
|
||||
{
|
||||
private static final Base64.Encoder ENCODER = Base64.getUrlEncoder();
|
||||
private static final String DEFAULT_HEADER = "{\"INFO\": \"this is not used or checked in our implementation\"}";
|
||||
private static final String DEFAULT_SIGNATURE = "we do not validate signature as we use the authorization code flow";
|
||||
|
||||
public static String encode(String idToken)
|
||||
{
|
||||
return stripPadding(ENCODER.encodeToString(DEFAULT_HEADER.getBytes())) + "." +
|
||||
stripPadding(ENCODER.encodeToString(idToken.getBytes())) + "." +
|
||||
stripPadding(ENCODER.encodeToString(DEFAULT_SIGNATURE.getBytes()));
|
||||
}
|
||||
|
||||
private static String stripPadding(String paddedBase64)
|
||||
{
|
||||
return paddedBase64.split("=")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic JWT for testing using argument supplied attributes.
|
||||
*/
|
||||
public static String createIdToken(String provider, String clientId, String subject, String name, long expiry)
|
||||
{
|
||||
return "{" +
|
||||
"\"iss\": \"" + provider + "\"," +
|
||||
"\"sub\": \"" + subject + "\"," +
|
||||
"\"aud\": \"" + clientId + "\"," +
|
||||
"\"exp\": " + expiry + "," +
|
||||
"\"name\": \"" + name + "\"," +
|
||||
"\"email\": \"" + name + "@example.com" + "\"" +
|
||||
"}";
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@ import org.eclipse.jetty.security.openid.OpenIdConfiguration;
|
|||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.session.FileSessionDataStoreFactory;
|
||||
import org.eclipse.jetty.tests.OpenIdProvider;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.security.Password;
|
||||
|
|
|
@ -1,402 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.security.openid;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee9.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.security.openid.OpenIdConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.statistic.CounterStatistic;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenIdProvider extends ContainerLifeCycle
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class);
|
||||
|
||||
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
|
||||
private static final String AUTH_PATH = "/auth";
|
||||
private static final String TOKEN_PATH = "/token";
|
||||
private static final String END_SESSION_PATH = "/end_session";
|
||||
private final Map<String, User> issuedAuthCodes = new HashMap<>();
|
||||
|
||||
protected final String clientId;
|
||||
protected final String clientSecret;
|
||||
protected final List<String> redirectUris = new ArrayList<>();
|
||||
private final ServerConnector connector;
|
||||
private final Server server;
|
||||
private int port = 0;
|
||||
private String provider;
|
||||
private User preAuthedUser;
|
||||
private final CounterStatistic loggedInUsers = new CounterStatistic();
|
||||
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
String clientId = "CLIENT_ID123";
|
||||
String clientSecret = "PASSWORD123";
|
||||
int port = 5771;
|
||||
String redirectUri = "http://localhost:8080/j_security_check";
|
||||
|
||||
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
|
||||
openIdProvider.addRedirectUri(redirectUri);
|
||||
openIdProvider.setPort(port);
|
||||
openIdProvider.start();
|
||||
try
|
||||
{
|
||||
openIdProvider.join();
|
||||
}
|
||||
finally
|
||||
{
|
||||
openIdProvider.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public OpenIdProvider(String clientId, String clientSecret)
|
||||
{
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
ServletContextHandler contextHandler = new ServletContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH);
|
||||
server.setHandler(contextHandler);
|
||||
|
||||
addBean(server);
|
||||
}
|
||||
|
||||
public void setIdTokenDuration(long duration)
|
||||
{
|
||||
_idTokenDuration = duration;
|
||||
}
|
||||
|
||||
public long getIdTokenDuration()
|
||||
{
|
||||
return _idTokenDuration;
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException
|
||||
{
|
||||
server.join();
|
||||
}
|
||||
|
||||
public OpenIdConfiguration getOpenIdConfiguration()
|
||||
{
|
||||
String provider = getProvider();
|
||||
String authEndpoint = provider + AUTH_PATH;
|
||||
String tokenEndpoint = provider + TOKEN_PATH;
|
||||
return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
|
||||
}
|
||||
|
||||
public CounterStatistic getLoggedInUsers()
|
||||
{
|
||||
return loggedInUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
connector.setPort(port);
|
||||
super.doStart();
|
||||
provider = "http://localhost:" + connector.getLocalPort();
|
||||
}
|
||||
|
||||
public void setPort(int port)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void setUser(User user)
|
||||
{
|
||||
this.preAuthedUser = user;
|
||||
}
|
||||
|
||||
public String getProvider()
|
||||
{
|
||||
if (!isStarted() && port == 0)
|
||||
throw new IllegalStateException("Port of OpenIdProvider not configured");
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void addRedirectUri(String uri)
|
||||
{
|
||||
redirectUris.add(uri);
|
||||
}
|
||||
|
||||
public class AuthEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
if (!clientId.equals(req.getParameter("client_id")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id");
|
||||
return;
|
||||
}
|
||||
|
||||
String redirectUri = req.getParameter("redirect_uri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
LOG.warn("invalid redirectUri {}", redirectUri);
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String scopeString = req.getParameter("scope");
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
|
||||
if (!scopes.contains("openid"))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"code".equals(req.getParameter("response_type")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = req.getParameter("state");
|
||||
if (state == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
if (preAuthedUser == null)
|
||||
{
|
||||
PrintWriter writer = resp.getWriter();
|
||||
resp.setContentType("text/html");
|
||||
writer.println("<h2>Login to OpenID Connect Provider</h2>");
|
||||
writer.println("<form action=\"" + AUTH_PATH + "\" method=\"post\">");
|
||||
writer.println("<input type=\"text\" autocomplete=\"off\" placeholder=\"Username\" name=\"username\" required>");
|
||||
writer.println("<input type=\"hidden\" name=\"redirectUri\" value=\"" + redirectUri + "\">");
|
||||
writer.println("<input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
|
||||
writer.println("<input type=\"submit\">");
|
||||
writer.println("</form>");
|
||||
}
|
||||
else
|
||||
{
|
||||
redirectUser(resp, preAuthedUser, redirectUri, state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String redirectUri = req.getParameter("redirectUri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = req.getParameter("state");
|
||||
if (state == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
String username = req.getParameter("username");
|
||||
if (username == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = new User(username);
|
||||
redirectUser(resp, user, redirectUri, state);
|
||||
}
|
||||
|
||||
public void redirectUser(HttpServletResponse response, User user, String redirectUri, String state) throws IOException
|
||||
{
|
||||
String authCode = UUID.randomUUID().toString().replace("-", "");
|
||||
issuedAuthCodes.put(authCode, user);
|
||||
|
||||
try
|
||||
{
|
||||
redirectUri += "?code=" + authCode + "&state=" + state;
|
||||
response.sendRedirect(response.encodeRedirectURL(redirectUri));
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
issuedAuthCodes.remove(authCode);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String code = req.getParameter("code");
|
||||
|
||||
if (!clientId.equals(req.getParameter("client_id")) ||
|
||||
!clientSecret.equals(req.getParameter("client_secret")) ||
|
||||
!redirectUris.contains(req.getParameter("redirect_uri")) ||
|
||||
!"authorization_code".equals(req.getParameter("grant_type")) ||
|
||||
code == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = issuedAuthCodes.remove(code);
|
||||
if (user == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code");
|
||||
return;
|
||||
}
|
||||
|
||||
String accessToken = "ABCDEFG";
|
||||
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
|
||||
String response = "{" +
|
||||
"\"access_token\": \"" + accessToken + "\"," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
|
||||
"\"expires_in\": " + accessTokenDuration + "," +
|
||||
"\"token_type\": \"Bearer\"" +
|
||||
"}";
|
||||
|
||||
loggedInUsers.increment();
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().print(response);
|
||||
}
|
||||
}
|
||||
|
||||
private class EndSessionEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
doPost(req, resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String idToken = req.getParameter("id_token_hint");
|
||||
if (idToken == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint");
|
||||
return;
|
||||
}
|
||||
|
||||
String logoutRedirect = req.getParameter("post_logout_redirect_uri");
|
||||
if (logoutRedirect == null)
|
||||
{
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
resp.getWriter().println("logout success on end_session_endpoint");
|
||||
return;
|
||||
}
|
||||
|
||||
loggedInUsers.decrement();
|
||||
resp.setContentType("text/plain");
|
||||
resp.sendRedirect(logoutRedirect);
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String discoveryDocument = "{" +
|
||||
"\"issuer\": \"" + provider + "\"," +
|
||||
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
|
||||
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
|
||||
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
|
||||
"}";
|
||||
|
||||
resp.getWriter().write(discoveryDocument);
|
||||
}
|
||||
}
|
||||
|
||||
public static class User
|
||||
{
|
||||
private final String subject;
|
||||
private final String name;
|
||||
|
||||
public User(String name)
|
||||
{
|
||||
this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name);
|
||||
}
|
||||
|
||||
public User(String subject, String name)
|
||||
{
|
||||
this.subject = subject;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getSubject()
|
||||
{
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getIdToken(String provider, String clientId, long duration)
|
||||
{
|
||||
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (!(obj instanceof User))
|
||||
return false;
|
||||
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(subject, name);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,8 +13,12 @@
|
|||
|
||||
package org.eclipse.jetty.ee9.servlet;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.EnumSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
@ -29,7 +33,6 @@ import jakarta.servlet.DispatcherType;
|
|||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.Servlet;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
|
@ -49,10 +52,10 @@ import org.eclipse.jetty.http.HttpTester;
|
|||
import org.eclipse.jetty.logging.StacklessLogging;
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -62,6 +65,7 @@ import static org.hamcrest.Matchers.containsString;
|
|||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
//@Disabled // TODO
|
||||
|
@ -105,6 +109,7 @@ public class ErrorPageTest
|
|||
_context.addServlet(ErrorAndStatusServlet.class, "/error-and-status/*");
|
||||
_context.addServlet(ErrorContentTypeCharsetWriterInitializedServlet.class, "/error-mime-charset-writer/*");
|
||||
_context.addServlet(ExceptionServlet.class, "/exception-servlet");
|
||||
_context.addServlet(AbortServlet.class, "/abort");
|
||||
|
||||
HandlerWrapper noopHandler = new HandlerWrapper()
|
||||
{
|
||||
|
@ -300,6 +305,34 @@ public class ErrorPageTest
|
|||
assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /fail/code"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAbortWithSendError() throws Exception
|
||||
{
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
ServerConnector connector = new ServerConnector(_server);
|
||||
connector.setPort(0);
|
||||
_server.addConnector(connector);
|
||||
connector.start();
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String request = """
|
||||
GET /abort HTTP/1.1\r
|
||||
Host: test\r
|
||||
\r
|
||||
""";
|
||||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
String line = in.readLine();
|
||||
assertNull(line);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorException() throws Exception
|
||||
{
|
||||
|
@ -871,4 +904,13 @@ public class ErrorPageTest
|
|||
super(rootCause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AbortServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
|
||||
{
|
||||
response.sendError(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,10 +23,21 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongo-java-driver</artifactId>
|
||||
<artifactId>bson</artifactId>
|
||||
<version>${mongodb.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongodb-driver-core</artifactId>
|
||||
<version>${mongodb.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongodb-driver-sync</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
|
|
|
@ -14,11 +14,15 @@ sessions
|
|||
sessions/mongo/${connection-type}
|
||||
|
||||
[files]
|
||||
maven://org.mongodb/mongo-java-driver/${mongodb.version}|lib/nosql/mongo-java-driver-${mongodb.version}.jar
|
||||
maven://org.mongodb/mongodb-driver-sync/${mongodb.version}|lib/nosql/mongodb-driver-sync-${mongodb.version}.jar
|
||||
maven://org.mongodb/mongodb-driver-core/${mongodb.version}|lib/nosql/mongodb-driver-core-${mongodb.version}.jar
|
||||
maven://org.mongodb/bson/${mongodb.version}|lib/nosql/bson-${mongodb.version}.jar
|
||||
|
||||
[lib]
|
||||
lib/jetty-nosql-${jetty.version}.jar
|
||||
lib/nosql/mongo-java-driver-${mongodb.version}.jar
|
||||
lib/nosql/mongodb-driver-sync-${mongodb.version}.jar
|
||||
lib/nosql/mongodb-driver-core-${mongodb.version}.jar
|
||||
lib/nosql/bson-${mongodb.version}.jar
|
||||
|
||||
[license]
|
||||
The java driver for the MongoDB document-based database system is hosted on GitHub and released under the Apache 2.0 license.
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
|
||||
module org.eclipse.jetty.nosql
|
||||
{
|
||||
requires transitive mongo.java.driver;
|
||||
requires transitive org.mongodb.driver.core;
|
||||
requires transitive org.mongodb.driver.sync.client;
|
||||
requires transitive org.mongodb.bson;
|
||||
requires transitive org.eclipse.jetty.session;
|
||||
|
||||
exports org.eclipse.jetty.nosql;
|
||||
|
|
|
@ -62,7 +62,7 @@ public abstract class NoSqlSessionDataStore extends ObjectStreamSessionDataStore
|
|||
|
||||
public Set<String> getAllAttributeNames()
|
||||
{
|
||||
return new HashSet<String>(_attributes.keySet());
|
||||
return new HashSet<>(_attributes.keySet());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,22 +15,26 @@ package org.eclipse.jetty.nosql.mongodb;
|
|||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import com.mongodb.BasicDBList;
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.BasicDBObjectBuilder;
|
||||
import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBCursor;
|
||||
import com.mongodb.DBObject;
|
||||
import com.mongodb.MongoException;
|
||||
import com.mongodb.WriteConcern;
|
||||
import com.mongodb.WriteResult;
|
||||
import com.mongodb.client.FindIterable;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import com.mongodb.client.model.Filters;
|
||||
import com.mongodb.client.model.IndexOptions;
|
||||
import com.mongodb.client.model.Indexes;
|
||||
import com.mongodb.client.model.Projections;
|
||||
import com.mongodb.client.model.UpdateOptions;
|
||||
import com.mongodb.client.result.UpdateResult;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.bson.types.Binary;
|
||||
import org.eclipse.jetty.nosql.NoSqlSessionDataStore;
|
||||
import org.eclipse.jetty.session.SessionContext;
|
||||
import org.eclipse.jetty.session.SessionData;
|
||||
|
@ -155,15 +159,15 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
/**
|
||||
* Access to MongoDB
|
||||
*/
|
||||
private DBCollection _dbSessions;
|
||||
private MongoCollection<Document> _dbSessions;
|
||||
|
||||
public void setDBCollection(DBCollection collection)
|
||||
public void setDBCollection(MongoCollection<Document> collection)
|
||||
{
|
||||
_dbSessions = collection;
|
||||
}
|
||||
|
||||
@ManagedAttribute(value = "DBCollection", readonly = true)
|
||||
public DBCollection getDBCollection()
|
||||
public MongoCollection<Document> getDBCollection()
|
||||
{
|
||||
return _dbSessions;
|
||||
}
|
||||
|
@ -171,7 +175,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
@Override
|
||||
public SessionData doLoad(String id) throws Exception
|
||||
{
|
||||
DBObject sessionDocument = _dbSessions.findOne(new BasicDBObject(__ID, id));
|
||||
Document sessionDocument = _dbSessions.find(Filters.eq(__ID, id)).first();
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -191,7 +195,8 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
Object version = MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__VERSION));
|
||||
Long lastSaved = (Long)MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__LASTSAVED));
|
||||
String lastNode = (String)MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__LASTNODE));
|
||||
byte[] attributes = (byte[])MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__ATTRIBUTES));
|
||||
Binary binary = ((Binary)MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__ATTRIBUTES)));
|
||||
byte[] attributes = binary == null ? null : binary.getData();
|
||||
|
||||
Long created = (Long)sessionDocument.get(__CREATED);
|
||||
Long accessed = (Long)sessionDocument.get(__ACCESSED);
|
||||
|
@ -202,7 +207,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
NoSqlSessionData data = null;
|
||||
|
||||
// get the session for the context
|
||||
DBObject sessionSubDocumentForContext = (DBObject)MongoUtils.getNestedValue(sessionDocument, getContextField());
|
||||
Document sessionSubDocumentForContext = (Document)MongoUtils.getNestedValue(sessionDocument, getContextField());
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("attrs {}", sessionSubDocumentForContext);
|
||||
|
@ -239,7 +244,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
else
|
||||
{
|
||||
//attributes have special serialized format
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(attributes);)
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(attributes))
|
||||
{
|
||||
deserializeAttributes(data, bais);
|
||||
}
|
||||
|
@ -269,17 +274,17 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
* Check if the session exists and if it does remove the context
|
||||
* associated with this session
|
||||
*/
|
||||
BasicDBObject mongoKey = new BasicDBObject(__ID, id);
|
||||
Bson filterId = Filters.eq(__ID, id);
|
||||
|
||||
DBObject sessionDocument = _dbSessions.findOne(new BasicDBObject(__ID, id));
|
||||
Document sessionDocument = _dbSessions.find(filterId).first();
|
||||
|
||||
if (sessionDocument != null)
|
||||
{
|
||||
DBObject c = (DBObject)MongoUtils.getNestedValue(sessionDocument, __CONTEXT);
|
||||
Document c = (Document)MongoUtils.getNestedValue(sessionDocument, __CONTEXT);
|
||||
if (c == null)
|
||||
{
|
||||
//delete whole doc
|
||||
_dbSessions.remove(mongoKey, WriteConcern.SAFE);
|
||||
_dbSessions.deleteOne(filterId);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -287,14 +292,14 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
if (contexts.isEmpty())
|
||||
{
|
||||
//delete whole doc
|
||||
_dbSessions.remove(mongoKey, WriteConcern.SAFE);
|
||||
_dbSessions.deleteOne(filterId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (contexts.size() == 1 && contexts.iterator().next().equals(getCanonicalContextId()))
|
||||
{
|
||||
//delete whole doc
|
||||
_dbSessions.remove(new BasicDBObject(__ID, id), WriteConcern.SAFE);
|
||||
_dbSessions.deleteOne(filterId);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -303,7 +308,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
BasicDBObject unsets = new BasicDBObject();
|
||||
unsets.put(getContextField(), 1);
|
||||
remove.put("$unset", unsets);
|
||||
_dbSessions.update(mongoKey, remove, false, false, WriteConcern.SAFE);
|
||||
_dbSessions.updateOne(filterId, remove);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
|
@ -315,12 +320,9 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
@Override
|
||||
public boolean doExists(String id) throws Exception
|
||||
{
|
||||
DBObject fields = new BasicDBObject();
|
||||
fields.put(__EXPIRY, 1);
|
||||
fields.put(__VALID, 1);
|
||||
fields.put(getContextSubfield(__VERSION), 1);
|
||||
|
||||
DBObject sessionDocument = _dbSessions.findOne(new BasicDBObject(__ID, id), fields);
|
||||
Bson projection = Projections.fields(Projections.include(__ID, __VALID, __EXPIRY, __VERSION, getContextField()), Projections.excludeId());
|
||||
Bson filterId = Filters.eq(__ID, id);
|
||||
Document sessionDocument = _dbSessions.find(filterId).projection(projection).first();
|
||||
|
||||
if (sessionDocument == null)
|
||||
return false; //doesn't exist
|
||||
|
@ -332,46 +334,31 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
Long expiry = (Long)sessionDocument.get(__EXPIRY);
|
||||
|
||||
//expired?
|
||||
if (expiry.longValue() > 0 && expiry.longValue() < System.currentTimeMillis())
|
||||
if (expiry != null && expiry > 0 && expiry < System.currentTimeMillis())
|
||||
return false; //it's expired
|
||||
|
||||
//does it exist for this context?
|
||||
Object version = MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__VERSION));
|
||||
if (version == null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
return version != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> doCheckExpired(Set<String> candidates, long time)
|
||||
{
|
||||
Set<String> expiredSessions = new HashSet<>();
|
||||
|
||||
//firstly ask mongo to verify if these candidate ids have expired - all of
|
||||
//these candidates will be for our node
|
||||
BasicDBObject query = new BasicDBObject();
|
||||
query.append(__ID, new BasicDBObject("$in", candidates));
|
||||
query.append(__EXPIRY, new BasicDBObject("$gt", 0).append("$lte", time));
|
||||
Bson query = Filters.and(
|
||||
Filters.in(__ID, candidates),
|
||||
Filters.gt(__EXPIRY, 0),
|
||||
Filters.lte(__EXPIRY, time));
|
||||
|
||||
DBCursor verifiedExpiredSessions = null;
|
||||
try
|
||||
{
|
||||
verifiedExpiredSessions = _dbSessions.find(query, new BasicDBObject(__ID, 1));
|
||||
for (DBObject session : verifiedExpiredSessions)
|
||||
{
|
||||
String id = (String)session.get(__ID);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} Mongo confirmed expired session {}", _context, id);
|
||||
expiredSessions.add(id);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (verifiedExpiredSessions != null)
|
||||
verifiedExpiredSessions.close();
|
||||
}
|
||||
|
||||
FindIterable<Document> verifiedExpiredSessions = _dbSessions.find(query); // , new BasicDBObject(__ID, 1)
|
||||
Set<String> expiredSessions =
|
||||
StreamSupport.stream(verifiedExpiredSessions.spliterator(), false)
|
||||
.map(document -> document.getString(__ID))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
//check through sessions that were candidates, but not found as expired.
|
||||
//they may no longer be persisted, in which case they are treated as expired.
|
||||
|
@ -398,37 +385,17 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
{
|
||||
// now ask mongo to find sessions for this context, last managed by any
|
||||
// node, that expired before timeLimit
|
||||
Set<String> expiredSessions = new HashSet<>();
|
||||
|
||||
BasicDBObject query = new BasicDBObject();
|
||||
BasicDBObject gt = new BasicDBObject(__EXPIRY, new BasicDBObject("$gt", 0));
|
||||
BasicDBObject lt = new BasicDBObject(__EXPIRY, new BasicDBObject("$lte", timeLimit));
|
||||
BasicDBList list = new BasicDBList();
|
||||
list.add(gt);
|
||||
list.add(lt);
|
||||
query.append("$and", list);
|
||||
|
||||
DBCursor oldExpiredSessions = null;
|
||||
try
|
||||
{
|
||||
BasicDBObject bo = new BasicDBObject(__ID, 1);
|
||||
bo.append(__EXPIRY, 1);
|
||||
|
||||
oldExpiredSessions = _dbSessions.find(query, bo);
|
||||
for (DBObject session : oldExpiredSessions)
|
||||
{
|
||||
String id = (String)session.get(__ID);
|
||||
Bson query = Filters.and(
|
||||
Filters.gt(__EXPIRY, 0),
|
||||
Filters.lte(__EXPIRY, timeLimit)
|
||||
);
|
||||
|
||||
//TODO we should verify if there is a session for my context, not any context
|
||||
expiredSessions.add(id);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (oldExpiredSessions != null)
|
||||
oldExpiredSessions.close();
|
||||
}
|
||||
|
||||
FindIterable<Document> documents = _dbSessions.find(query);
|
||||
Set<String> expiredSessions = StreamSupport.stream(documents.spliterator(), false)
|
||||
.map(document -> document.getString(__ID))
|
||||
.collect(Collectors.toSet());
|
||||
return expiredSessions;
|
||||
}
|
||||
|
||||
|
@ -438,9 +405,11 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
//Delete all session documents where the expiry time (which is always the most
|
||||
//up-to-date expiry of all contexts sharing that session id) has already past as
|
||||
//at the timeLimit.
|
||||
BasicDBObject query = new BasicDBObject();
|
||||
query.append(__EXPIRY, new BasicDBObject("$gt", 0).append("$lte", timeLimit));
|
||||
_dbSessions.remove(query, WriteConcern.SAFE);
|
||||
Bson query = Filters.and(
|
||||
Filters.gt(__EXPIRY, 0),
|
||||
Filters.lte(__EXPIRY, timeLimit)
|
||||
);
|
||||
_dbSessions.deleteMany(query);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -458,8 +427,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
public void doStore(String id, SessionData data, long lastSaveTime) throws Exception
|
||||
{
|
||||
// Form query for upsert
|
||||
final BasicDBObject key = new BasicDBObject(__ID, id);
|
||||
|
||||
Bson key = Filters.eq(__ID, id);;
|
||||
// Form updates
|
||||
BasicDBObject update = new BasicDBObject();
|
||||
boolean upsert = false;
|
||||
|
@ -487,12 +455,13 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
sets.put(getContextSubfield(__LASTNODE), data.getLastNode());
|
||||
version = ((Number)version).longValue() + 1L;
|
||||
((NoSqlSessionData)data).setVersion(version);
|
||||
update.put("$inc", _version1);
|
||||
// what is this?? this field is used no where...
|
||||
//sets.put("$inc", _version1);
|
||||
//if max idle time and/or expiry is smaller for this context, then choose that for the whole session doc
|
||||
BasicDBObject fields = new BasicDBObject();
|
||||
fields.append(__MAX_IDLE, true);
|
||||
fields.append(__EXPIRY, true);
|
||||
DBObject o = _dbSessions.findOne(new BasicDBObject("id", id), fields);
|
||||
Document o = _dbSessions.find(key).first();
|
||||
if (o != null)
|
||||
{
|
||||
Long tmpLong = (Long)o.get(__MAX_IDLE);
|
||||
|
@ -516,37 +485,39 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore
|
|||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();)
|
||||
{
|
||||
serializeAttributes(data, baos);
|
||||
sets.put(getContextSubfield(__ATTRIBUTES), baos.toByteArray());
|
||||
Binary binary = new Binary(baos.toByteArray());
|
||||
sets.put(getContextSubfield(__ATTRIBUTES), binary);
|
||||
}
|
||||
|
||||
// Do the upsert
|
||||
if (!sets.isEmpty())
|
||||
update.put("$set", sets);
|
||||
|
||||
WriteResult res = _dbSessions.update(key, update, upsert, false, WriteConcern.SAFE);
|
||||
UpdateResult res = _dbSessions.updateOne(key, update, new UpdateOptions().upsert(upsert));
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Save:db.sessions.update( {}, {},{} )", key, update, res);
|
||||
}
|
||||
|
||||
protected void ensureIndexes() throws MongoException
|
||||
{
|
||||
_version1 = new BasicDBObject(getContextSubfield(__VERSION), 1);
|
||||
DBObject idKey = BasicDBObjectBuilder.start().add("id", 1).get();
|
||||
_dbSessions.createIndex(idKey,
|
||||
BasicDBObjectBuilder.start()
|
||||
.add("name", "id_1")
|
||||
.add("ns", _dbSessions.getFullName())
|
||||
.add("sparse", false)
|
||||
.add("unique", true)
|
||||
.get());
|
||||
|
||||
DBObject versionKey = BasicDBObjectBuilder.start().add("id", 1).add("version", 1).get();
|
||||
_dbSessions.createIndex(versionKey, BasicDBObjectBuilder.start()
|
||||
.add("name", "id_1_version_1")
|
||||
.add("ns", _dbSessions.getFullName())
|
||||
.add("sparse", false)
|
||||
.add("unique", true)
|
||||
.get());
|
||||
var indexes =
|
||||
StreamSupport.stream(_dbSessions.listIndexes().spliterator(), false)
|
||||
.toList();
|
||||
var indexesNames = indexes.stream().map(document -> document.getString("name")).toList();
|
||||
if (!indexesNames.contains("id_1"))
|
||||
{
|
||||
String createResult = _dbSessions.createIndex(Indexes.text("id"),
|
||||
new IndexOptions().unique(true).name("id_1").sparse(false));
|
||||
LOG.info("create index {}, result: {}", "id_1", createResult);
|
||||
}
|
||||
if (!indexesNames.contains("id_1_version_1"))
|
||||
{
|
||||
// Command failed with error 67 (CannotCreateIndex): 'only one text index per collection allowed, found existing text index "id_1"'
|
||||
String createResult = _dbSessions.createIndex(
|
||||
Indexes.compoundIndex(Indexes.descending("id"), Indexes.descending("version")),
|
||||
new IndexOptions().unique(false).name("id_1_version_1").sparse(false));
|
||||
LOG.info("create index {}, result: {}", "id_1_version_1", createResult);
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Done ensure Mongodb indexes existing");
|
||||
//TODO perhaps index on expiry time?
|
||||
|
|
|
@ -15,8 +15,8 @@ package org.eclipse.jetty.nosql.mongodb;
|
|||
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import com.mongodb.MongoClient;
|
||||
import com.mongodb.MongoClientURI;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import org.eclipse.jetty.session.AbstractSessionDataStoreFactory;
|
||||
import org.eclipse.jetty.session.SessionDataStore;
|
||||
import org.eclipse.jetty.session.SessionManager;
|
||||
|
@ -136,14 +136,14 @@ public class MongoSessionDataStoreFactory extends AbstractSessionDataStoreFactor
|
|||
MongoClient mongo;
|
||||
|
||||
if (!StringUtil.isBlank(getConnectionString()))
|
||||
mongo = new MongoClient(new MongoClientURI(getConnectionString()));
|
||||
mongo = MongoClients.create(getConnectionString());
|
||||
else if (!StringUtil.isBlank(getHost()) && getPort() != -1)
|
||||
mongo = new MongoClient(getHost(), getPort());
|
||||
mongo = MongoClients.create("mongodb://" + getHost() + ":" + getPort());
|
||||
else if (!StringUtil.isBlank(getHost()))
|
||||
mongo = new MongoClient(getHost());
|
||||
mongo = MongoClients.create("mongodb://" + getHost());
|
||||
else
|
||||
mongo = new MongoClient();
|
||||
store.setDBCollection(mongo.getDB(getDbName()).getCollection(getCollectionName()));
|
||||
mongo = MongoClients.create();
|
||||
store.setDBCollection(mongo.getDatabase(getDbName()).getCollection(getCollectionName()));
|
||||
return store;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.DBObject;
|
||||
import org.bson.Document;
|
||||
import org.bson.types.Binary;
|
||||
import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
|
||||
import org.eclipse.jetty.util.URIUtil;
|
||||
|
||||
|
@ -40,6 +41,13 @@ public class MongoUtils
|
|||
{
|
||||
return valueToDecode;
|
||||
}
|
||||
else if (valueToDecode instanceof Binary)
|
||||
{
|
||||
final byte[] decodeObject = ((Binary)valueToDecode).getData();
|
||||
final ByteArrayInputStream bais = new ByteArrayInputStream(decodeObject);
|
||||
final ClassLoadingObjectInputStream objectInputStream = new ClassLoadingObjectInputStream(bais);
|
||||
return objectInputStream.readUnshared();
|
||||
}
|
||||
else if (valueToDecode instanceof byte[])
|
||||
{
|
||||
final byte[] decodeObject = (byte[])valueToDecode;
|
||||
|
@ -47,13 +55,13 @@ public class MongoUtils
|
|||
final ClassLoadingObjectInputStream objectInputStream = new ClassLoadingObjectInputStream(bais);
|
||||
return objectInputStream.readUnshared();
|
||||
}
|
||||
else if (valueToDecode instanceof DBObject)
|
||||
else if (valueToDecode instanceof Document)
|
||||
{
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
for (String name : ((DBObject)valueToDecode).keySet())
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (String name : ((Document)valueToDecode).keySet())
|
||||
{
|
||||
String attr = decodeName(name);
|
||||
map.put(attr, decodeValue(((DBObject)valueToDecode).get(name)));
|
||||
map.put(attr, decodeValue(((Document)valueToDecode).get(name)));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
@ -107,19 +115,19 @@ public class MongoUtils
|
|||
/**
|
||||
* Dig through a given dbObject for the nested value
|
||||
*
|
||||
* @param dbObject the mongo object to search
|
||||
* @param sessionDocument the mongo document to search
|
||||
* @param nestedKey the field key to find
|
||||
* @return the value of the field key
|
||||
*/
|
||||
public static Object getNestedValue(DBObject dbObject, String nestedKey)
|
||||
public static Object getNestedValue(Document sessionDocument, String nestedKey)
|
||||
{
|
||||
String[] keyChain = nestedKey.split("\\.");
|
||||
|
||||
DBObject temp = dbObject;
|
||||
Document temp = sessionDocument;
|
||||
|
||||
for (int i = 0; i < keyChain.length - 1; ++i)
|
||||
{
|
||||
temp = (DBObject)temp.get(keyChain[i]);
|
||||
temp = (Document)temp.get(keyChain[i]);
|
||||
|
||||
if (temp == null)
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-core</artifactId>
|
||||
<artifactId>jetty-integrations</artifactId>
|
||||
<version>12.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>jetty-openid</artifactId>
|
||||
|
@ -47,6 +47,11 @@
|
|||
<artifactId>jetty-slf4j-impl</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-test-common</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
|
@ -16,6 +16,7 @@ package org.eclipse.jetty.security.openid;
|
|||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.tests.JwtEncoder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
|
@ -43,6 +43,7 @@ import org.eclipse.jetty.server.handler.ContextHandler;
|
|||
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
|
||||
import org.eclipse.jetty.session.FileSessionDataStoreFactory;
|
||||
import org.eclipse.jetty.session.SessionHandler;
|
||||
import org.eclipse.jetty.tests.OpenIdProvider;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.IO;
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
<modules>
|
||||
<module>jetty-ethereum</module>
|
||||
<module>jetty-openid</module>
|
||||
<module>jetty-gcloud</module>
|
||||
<module>jetty-hazelcast</module>
|
||||
<module>jetty-infinispan</module>
|
||||
|
|
14
pom.xml
14
pom.xml
|
@ -373,8 +373,8 @@
|
|||
<maven.version>3.9.0</maven.version>
|
||||
<maven.war.plugin.version>3.4.0</maven.war.plugin.version>
|
||||
<mina.core.version>2.2.3</mina.core.version>
|
||||
<mongo.docker.version>3.2.20</mongo.docker.version>
|
||||
<mongodb.version>3.12.14</mongodb.version>
|
||||
<mongo.docker.version>5.0.26</mongo.docker.version>
|
||||
<mongodb.version>5.1.3</mongodb.version>
|
||||
<netty.version>4.1.109.Final</netty.version>
|
||||
<openpojo.version>0.9.1</openpojo.version>
|
||||
<org.osgi.annotation.version>8.1.0</org.osgi.annotation.version>
|
||||
|
@ -1060,6 +1060,11 @@
|
|||
<artifactId>jetty-test-session-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-test-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-testers</artifactId>
|
||||
|
@ -1191,6 +1196,11 @@
|
|||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>${mariadb.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongodb-driver-sync</artifactId>
|
||||
<version>${mongodb.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mortbay.jetty.quiche</groupId>
|
||||
<artifactId>jetty-quiche-native</artifactId>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>tests</artifactId>
|
||||
<version>12.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>jetty-test-common</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Tests :: Test Utilities</name>
|
||||
|
||||
<properties>
|
||||
<bundle-symbolic-name>${project.groupId}.testers</bundle-symbolic-name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-util</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -11,7 +11,7 @@
|
|||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.openid;
|
||||
package org.eclipse.jetty.tests;
|
||||
|
||||
import java.util.Base64;
|
||||
|
|
@ -0,0 +1,421 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.statistic.CounterStatistic;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenIdProvider extends ContainerLifeCycle
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class);
|
||||
|
||||
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
|
||||
private static final String AUTH_PATH = "/auth";
|
||||
private static final String TOKEN_PATH = "/token";
|
||||
private static final String END_SESSION_PATH = "/end_session";
|
||||
private final Map<String, User> issuedAuthCodes = new HashMap<>();
|
||||
|
||||
protected final String clientId;
|
||||
protected final String clientSecret;
|
||||
protected final List<String> redirectUris = new ArrayList<>();
|
||||
private final ServerConnector connector;
|
||||
private final Server server;
|
||||
private int port = 0;
|
||||
private String provider;
|
||||
private User preAuthedUser;
|
||||
private final CounterStatistic loggedInUsers = new CounterStatistic();
|
||||
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
String clientId = "CLIENT_ID123";
|
||||
String clientSecret = "PASSWORD123";
|
||||
int port = 5771;
|
||||
String redirectUri = "http://localhost:8080/j_security_check";
|
||||
|
||||
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
|
||||
openIdProvider.addRedirectUri(redirectUri);
|
||||
openIdProvider.setPort(port);
|
||||
openIdProvider.start();
|
||||
try
|
||||
{
|
||||
openIdProvider.join();
|
||||
}
|
||||
finally
|
||||
{
|
||||
openIdProvider.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public OpenIdProvider()
|
||||
{
|
||||
this("clientId" + StringUtil.randomAlphaNumeric(4), StringUtil.randomAlphaNumeric(10));
|
||||
}
|
||||
|
||||
public OpenIdProvider(String clientId, String clientSecret)
|
||||
{
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
server.setHandler(new OpenIdProviderHandler());
|
||||
addBean(server);
|
||||
}
|
||||
|
||||
public String getClientId()
|
||||
{
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public String getClientSecret()
|
||||
{
|
||||
return clientSecret;
|
||||
}
|
||||
|
||||
public void setIdTokenDuration(long duration)
|
||||
{
|
||||
_idTokenDuration = duration;
|
||||
}
|
||||
|
||||
public long getIdTokenDuration()
|
||||
{
|
||||
return _idTokenDuration;
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException
|
||||
{
|
||||
server.join();
|
||||
}
|
||||
|
||||
public CounterStatistic getLoggedInUsers()
|
||||
{
|
||||
return loggedInUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
connector.setPort(port);
|
||||
super.doStart();
|
||||
provider = "http://localhost:" + connector.getLocalPort();
|
||||
}
|
||||
|
||||
public void setPort(int port)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void setUser(User user)
|
||||
{
|
||||
this.preAuthedUser = user;
|
||||
}
|
||||
|
||||
public String getProvider()
|
||||
{
|
||||
if (!isStarted() && port == 0)
|
||||
throw new IllegalStateException("Port of OpenIdProvider not configured");
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void addRedirectUri(String uri)
|
||||
{
|
||||
redirectUris.add(uri);
|
||||
}
|
||||
|
||||
public class OpenIdProviderHandler extends Handler.Abstract
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
String pathInContext = Request.getPathInContext(request);
|
||||
switch (pathInContext)
|
||||
{
|
||||
case CONFIG_PATH -> doGetConfigServlet(request, response, callback);
|
||||
case AUTH_PATH -> doAuthEndpoint(request, response, callback);
|
||||
case TOKEN_PATH -> doTokenEndpoint(request, response, callback);
|
||||
case END_SESSION_PATH -> doEndSessionEndpoint(request, response, callback);
|
||||
default -> Response.writeError(request, response, callback, HttpStatus.NOT_FOUND_404);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected void doAuthEndpoint(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
String method = request.getMethod();
|
||||
switch (method)
|
||||
{
|
||||
case "GET" -> doGetAuthEndpoint(request, response, callback);
|
||||
case "POST" -> doPostAuthEndpoint(request, response, callback);
|
||||
default -> throw new BadMessageException("Unsupported HTTP method: " + method);
|
||||
}
|
||||
}
|
||||
|
||||
protected void doGetAuthEndpoint(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
Fields parameters = Request.getParameters(request);
|
||||
|
||||
if (!clientId.equals(parameters.getValue("client_id")))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid client_id");
|
||||
return;
|
||||
}
|
||||
|
||||
String redirectUri = parameters.getValue("redirect_uri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
LOG.warn("invalid redirectUri {}", redirectUri);
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String scopeString = parameters.getValue("scope");
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
|
||||
if (!scopes.contains("openid"))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no openid scope");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"code".equals(parameters.getValue("response_type")))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "response_type must be code");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = parameters.getValue("state");
|
||||
if (state == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
if (preAuthedUser == null)
|
||||
{
|
||||
String responseContent = String.format("""
|
||||
<h2>Login to OpenID Connect Provider</h2>
|
||||
<form action="%s" method="post">
|
||||
<input type="text" autocomplete="off" placeholder="Username" name="username" required>
|
||||
<input type="hidden" name="redirectUri" value="%s">
|
||||
<input type="hidden" name="state" value="%s">
|
||||
<input type="submit">
|
||||
</form>
|
||||
""", AUTH_PATH, redirectUri, state);
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html");
|
||||
response.write(true, BufferUtil.toBuffer(responseContent), callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
redirectUser(request, response, callback, preAuthedUser, redirectUri, state);
|
||||
}
|
||||
}
|
||||
|
||||
protected void doPostAuthEndpoint(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
Fields parameters = Request.getParameters(request);
|
||||
String redirectUri = parameters.getValue("redirectUri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = parameters.getValue("state");
|
||||
if (state == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
String username = parameters.getValue("username");
|
||||
if (username == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no username");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = new User(username);
|
||||
redirectUser(request, response, callback, user, redirectUri, state);
|
||||
}
|
||||
|
||||
public void redirectUser(Request request, Response response, Callback callback, User user, String redirectUri, String state) throws IOException
|
||||
{
|
||||
String authCode = UUID.randomUUID().toString().replace("-", "");
|
||||
issuedAuthCodes.put(authCode, user);
|
||||
|
||||
try
|
||||
{
|
||||
redirectUri += "?code=" + authCode + "&state=" + state;
|
||||
Response.sendRedirect(request, response, callback, redirectUri);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
issuedAuthCodes.remove(authCode);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
protected void doTokenEndpoint(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
if (!HttpMethod.POST.is(request.getMethod()))
|
||||
throw new BadMessageException("Unsupported HTTP method for token Endpoint: " + request.getMethod());
|
||||
|
||||
Fields parameters = Request.getParameters(request);
|
||||
String code = parameters.getValue("code");
|
||||
|
||||
if (!clientId.equals(parameters.getValue("client_id")) ||
|
||||
!clientSecret.equals(parameters.getValue("client_secret")) ||
|
||||
!redirectUris.contains(parameters.getValue("redirect_uri")) ||
|
||||
!"authorization_code".equals(parameters.getValue("grant_type")) ||
|
||||
code == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "bad auth request");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = issuedAuthCodes.remove(code);
|
||||
if (user == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid auth code");
|
||||
return;
|
||||
}
|
||||
|
||||
String accessToken = "ABCDEFG";
|
||||
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
|
||||
String responseContent = "{" +
|
||||
"\"access_token\": \"" + accessToken + "\"," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
|
||||
"\"expires_in\": " + accessTokenDuration + "," +
|
||||
"\"token_type\": \"Bearer\"" +
|
||||
"}";
|
||||
|
||||
loggedInUsers.increment();
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
|
||||
response.write(true, BufferUtil.toBuffer(responseContent), callback);
|
||||
}
|
||||
|
||||
protected void doEndSessionEndpoint(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
Fields parameters = Request.getParameters(request);
|
||||
String idToken = parameters.getValue("id_token_hint");
|
||||
if (idToken == null)
|
||||
{
|
||||
Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "no id_token_hint");
|
||||
return;
|
||||
}
|
||||
|
||||
String logoutRedirect = parameters.getValue("post_logout_redirect_uri");
|
||||
if (logoutRedirect == null)
|
||||
{
|
||||
response.setStatus(HttpStatus.OK_200);
|
||||
response.write(true, BufferUtil.toBuffer("logout success on end_session_endpoint"), callback);
|
||||
return;
|
||||
}
|
||||
|
||||
loggedInUsers.decrement();
|
||||
Response.sendRedirect(request, response, callback, logoutRedirect);
|
||||
}
|
||||
|
||||
protected void doGetConfigServlet(Request request, Response response, Callback callback) throws IOException
|
||||
{
|
||||
String discoveryDocument = "{" +
|
||||
"\"issuer\": \"" + provider + "\"," +
|
||||
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
|
||||
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
|
||||
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
|
||||
"}";
|
||||
|
||||
response.write(true, BufferUtil.toBuffer(discoveryDocument), callback);
|
||||
}
|
||||
|
||||
public static class User
|
||||
{
|
||||
private final String subject;
|
||||
private final String name;
|
||||
|
||||
public User(String name)
|
||||
{
|
||||
this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name);
|
||||
}
|
||||
|
||||
public User(String subject, String name)
|
||||
{
|
||||
this.subject = subject;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getSubject()
|
||||
{
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getIdToken(String provider, String clientId, long duration)
|
||||
{
|
||||
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (!(obj instanceof User))
|
||||
return false;
|
||||
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(subject, name);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -101,7 +101,7 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongo-java-driver</artifactId>
|
||||
<artifactId>mongodb-driver-sync</artifactId>
|
||||
<version>${mongodb.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
|
|
@ -18,13 +18,20 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.ObjectOutputStream;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Map;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBObject;
|
||||
import com.mongodb.MongoClient;
|
||||
import com.mongodb.MongoException;
|
||||
import com.mongodb.WriteConcern;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import com.mongodb.client.model.CreateCollectionOptions;
|
||||
import com.mongodb.client.model.Filters;
|
||||
import com.mongodb.client.model.UpdateOptions;
|
||||
import org.bson.Document;
|
||||
import org.bson.types.Binary;
|
||||
import org.eclipse.jetty.nosql.mongodb.MongoSessionDataStore;
|
||||
import org.eclipse.jetty.nosql.mongodb.MongoSessionDataStoreFactory;
|
||||
import org.eclipse.jetty.nosql.mongodb.MongoUtils;
|
||||
|
@ -57,7 +64,7 @@ public class MongoTestHelper
|
|||
|
||||
static
|
||||
{
|
||||
mongo = new MongoDBContainer(DockerImageName.parse("mongo:" + System.getProperty("mongo.docker.version", "3.2.20")))
|
||||
mongo = new MongoDBContainer(DockerImageName.parse("mongo:" + System.getProperty("mongo.docker.version", "5.0.26")))
|
||||
.withLogConsumer(new Slf4jLogConsumer(MONGO_LOG));
|
||||
long start = System.currentTimeMillis();
|
||||
mongo.start();
|
||||
|
@ -65,21 +72,21 @@ public class MongoTestHelper
|
|||
mongoPort = mongo.getMappedPort(MONGO_PORT);
|
||||
LOG.info("Mongo container started for {}:{} - {}ms", mongoHost, mongoPort,
|
||||
System.currentTimeMillis() - start);
|
||||
mongoClient = new MongoClient(mongoHost, mongoPort);
|
||||
mongoClient = MongoClients.create(mongo.getConnectionString());
|
||||
}
|
||||
|
||||
public static MongoClient getMongoClient() throws UnknownHostException
|
||||
{
|
||||
if (mongoClient == null)
|
||||
{
|
||||
mongoClient = new MongoClient(mongoHost, mongoPort);
|
||||
mongoClient = MongoClients.create(mongo.getConnectionString());
|
||||
}
|
||||
return mongoClient;
|
||||
}
|
||||
|
||||
public static void dropCollection(String dbName, String collectionName) throws Exception
|
||||
{
|
||||
getMongoClient().getDB(dbName).getCollection(collectionName).drop();
|
||||
getMongoClient().getDatabase(dbName).getCollection(collectionName).withWriteConcern(WriteConcern.JOURNALED).drop();
|
||||
}
|
||||
|
||||
public static void shutdown() throws Exception
|
||||
|
@ -89,12 +96,14 @@ public class MongoTestHelper
|
|||
|
||||
public static void createCollection(String dbName, String collectionName) throws UnknownHostException, MongoException
|
||||
{
|
||||
getMongoClient().getDB(dbName).createCollection(collectionName, null);
|
||||
if (StreamSupport.stream(getMongoClient().getDatabase(dbName).listCollectionNames().spliterator(), false)
|
||||
.filter(collectionName::equals).findAny().isEmpty())
|
||||
getMongoClient().getDatabase(dbName).withWriteConcern(WriteConcern.JOURNALED).createCollection(collectionName, new CreateCollectionOptions());
|
||||
}
|
||||
|
||||
public static DBCollection getCollection(String dbName, String collectionName) throws UnknownHostException, MongoException
|
||||
public static MongoCollection<Document> getCollection(String dbName, String collectionName) throws UnknownHostException, MongoException
|
||||
{
|
||||
return getMongoClient().getDB(dbName).getCollection(collectionName);
|
||||
return getMongoClient().getDatabase(dbName).getCollection(collectionName);
|
||||
}
|
||||
|
||||
public static MongoSessionDataStoreFactory newSessionDataStoreFactory(String dbName, String collectionName)
|
||||
|
@ -110,13 +119,13 @@ public class MongoTestHelper
|
|||
public static boolean checkSessionExists(String id, String dbName, String collectionName)
|
||||
throws Exception
|
||||
{
|
||||
DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName);
|
||||
MongoCollection<Document> collection = getMongoClient().getDatabase(dbName).getCollection(collectionName);
|
||||
|
||||
DBObject fields = new BasicDBObject();
|
||||
fields.put(MongoSessionDataStore.__EXPIRY, 1);
|
||||
fields.put(MongoSessionDataStore.__VALID, 1);
|
||||
|
||||
DBObject sessionDocument = collection.findOne(new BasicDBObject(MongoSessionDataStore.__ID, id), fields);
|
||||
Document sessionDocument = collection.find(Filters.eq(MongoSessionDataStore.__ID, id)).first();
|
||||
|
||||
if (sessionDocument == null)
|
||||
return false; //doesn't exist
|
||||
|
@ -127,19 +136,19 @@ public class MongoTestHelper
|
|||
public static boolean checkSessionPersisted(SessionData data, String dbName, String collectionName)
|
||||
throws Exception
|
||||
{
|
||||
DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName);
|
||||
MongoCollection<Document> collection = getMongoClient().getDatabase(dbName).getCollection(collectionName);
|
||||
|
||||
DBObject fields = new BasicDBObject();
|
||||
|
||||
DBObject sessionDocument = collection.findOne(new BasicDBObject(MongoSessionDataStore.__ID, data.getId()), fields);
|
||||
Document sessionDocument = collection.find(Filters.eq(MongoSessionDataStore.__ID, data.getId())).first();
|
||||
if (sessionDocument == null)
|
||||
return false; //doesn't exist
|
||||
|
||||
LOG.debug("{}", sessionDocument);
|
||||
|
||||
Boolean valid = (Boolean)sessionDocument.get(MongoSessionDataStore.__VALID);
|
||||
boolean valid = (Boolean)sessionDocument.get(MongoSessionDataStore.__VALID);
|
||||
|
||||
if (valid == null || !valid)
|
||||
if (!valid)
|
||||
return false;
|
||||
|
||||
Long created = (Long)sessionDocument.get(MongoSessionDataStore.__CREATED);
|
||||
|
@ -154,8 +163,8 @@ public class MongoTestHelper
|
|||
MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTSAVED);
|
||||
String lastNode = (String)MongoUtils.getNestedValue(sessionDocument,
|
||||
MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTNODE);
|
||||
byte[] attributes = (byte[])MongoUtils.getNestedValue(sessionDocument,
|
||||
MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__ATTRIBUTES);
|
||||
byte[] attributes = ((Binary)MongoUtils.getNestedValue(sessionDocument,
|
||||
MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__ATTRIBUTES)).getData();
|
||||
|
||||
assertEquals(data.getCreated(), created.longValue());
|
||||
assertEquals(data.getAccessed(), accessed.longValue());
|
||||
|
@ -167,8 +176,8 @@ public class MongoTestHelper
|
|||
assertNotNull(lastSaved);
|
||||
|
||||
// get the session for the context
|
||||
DBObject sessionSubDocumentForContext =
|
||||
(DBObject)MongoUtils.getNestedValue(sessionDocument,
|
||||
Document sessionSubDocumentForContext =
|
||||
(Document)MongoUtils.getNestedValue(sessionDocument,
|
||||
MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath());
|
||||
|
||||
assertNotNull(sessionSubDocumentForContext);
|
||||
|
@ -202,10 +211,7 @@ public class MongoTestHelper
|
|||
String collectionName)
|
||||
throws Exception
|
||||
{
|
||||
DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName);
|
||||
|
||||
// Form query for upsert
|
||||
BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id);
|
||||
MongoCollection<Document> collection = getMongoClient().getDatabase(dbName).getCollection(collectionName);
|
||||
|
||||
// Form updates
|
||||
BasicDBObject update = new BasicDBObject();
|
||||
|
@ -236,12 +242,12 @@ public class MongoTestHelper
|
|||
ObjectOutputStream oos = new ObjectOutputStream(baos))
|
||||
{
|
||||
SessionData.serializeAttributes(tmp, oos);
|
||||
sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, baos.toByteArray());
|
||||
sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, new Binary(baos.toByteArray()));
|
||||
}
|
||||
}
|
||||
|
||||
update.put("$set", sets);
|
||||
collection.update(key, update, upsert, false, WriteConcern.SAFE);
|
||||
collection.updateOne(Filters.eq(MongoSessionDataStore.__ID, id), update, new UpdateOptions().upsert(true));
|
||||
}
|
||||
|
||||
public static void createSession(String id, String contextPath, String vhost,
|
||||
|
@ -252,7 +258,7 @@ public class MongoTestHelper
|
|||
throws Exception
|
||||
{
|
||||
|
||||
DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName);
|
||||
MongoCollection<Document> collection = getMongoClient().getDatabase(dbName).getCollection(collectionName);
|
||||
|
||||
// Form query for upsert
|
||||
BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id);
|
||||
|
@ -283,12 +289,12 @@ public class MongoTestHelper
|
|||
ObjectOutputStream oos = new ObjectOutputStream(baos))
|
||||
{
|
||||
SessionData.serializeAttributes(tmp, oos);
|
||||
sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, baos.toByteArray());
|
||||
sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, new Binary(baos.toByteArray()));
|
||||
}
|
||||
}
|
||||
|
||||
update.put("$set", sets);
|
||||
collection.update(key, update, upsert, false, WriteConcern.SAFE);
|
||||
collection.updateOne(key, update, new UpdateOptions().upsert(true));
|
||||
}
|
||||
|
||||
public static void createLegacySession(String id, String contextPath, String vhost,
|
||||
|
@ -299,7 +305,7 @@ public class MongoTestHelper
|
|||
throws Exception
|
||||
{
|
||||
//make old-style session to test if we can retrieve it
|
||||
DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName);
|
||||
MongoCollection<Document> collection = getMongoClient().getDatabase(dbName).getCollection(collectionName);
|
||||
|
||||
// Form query for upsert
|
||||
BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id);
|
||||
|
@ -333,6 +339,6 @@ public class MongoTestHelper
|
|||
}
|
||||
}
|
||||
update.put("$set", sets);
|
||||
collection.update(key, update, upsert, false, WriteConcern.SAFE);
|
||||
collection.updateOne(key, update, new UpdateOptions().upsert(true));
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
<module>jetty-jmh</module>
|
||||
<module>jetty-test-multipart</module>
|
||||
<module>jetty-test-session-common</module>
|
||||
<module>jetty-test-common</module>
|
||||
<module>test-cross-context-dispatch</module>
|
||||
<module>test-distribution</module>
|
||||
<module>test-integration</module>
|
||||
|
|
|
@ -35,7 +35,7 @@ public class MongodbSessionDistributionTests extends AbstractSessionDistribution
|
|||
|
||||
private static final int MONGO_PORT = 27017;
|
||||
|
||||
final String imageName = "mongo:" + System.getProperty("mongo.docker.version", "3.2.20");
|
||||
final String imageName = "mongo:" + System.getProperty("mongo.docker.version", "5.0.26");
|
||||
|
||||
final MongoDBContainer mongoDBContainer =
|
||||
new MongoDBContainer(DockerImageName.parse(imageName))
|
||||
|
|
|
@ -60,6 +60,11 @@
|
|||
<type>war</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-test-common</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-testers</artifactId>
|
||||
|
|
|
@ -17,8 +17,8 @@ import java.nio.file.Path;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.ee10.tests.distribution.openid.OpenIdProvider;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.tests.OpenIdProvider;
|
||||
import org.eclipse.jetty.tests.distribution.AbstractJettyHomeTest;
|
||||
import org.eclipse.jetty.tests.testers.JettyHomeTester;
|
||||
import org.eclipse.jetty.tests.testers.Tester;
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.tests.distribution.openid;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* A basic JWT encoder for testing purposes.
|
||||
*/
|
||||
public class JwtEncoder
|
||||
{
|
||||
private static final Base64.Encoder ENCODER = Base64.getUrlEncoder();
|
||||
private static final String DEFAULT_HEADER = "{\"INFO\": \"this is not used or checked in our implementation\"}";
|
||||
private static final String DEFAULT_SIGNATURE = "we do not validate signature as we use the authorization code flow";
|
||||
|
||||
public static String encode(String idToken)
|
||||
{
|
||||
return stripPadding(ENCODER.encodeToString(DEFAULT_HEADER.getBytes())) + "." +
|
||||
stripPadding(ENCODER.encodeToString(idToken.getBytes())) + "." +
|
||||
stripPadding(ENCODER.encodeToString(DEFAULT_SIGNATURE.getBytes()));
|
||||
}
|
||||
|
||||
private static String stripPadding(String paddedBase64)
|
||||
{
|
||||
return paddedBase64.split("=")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic JWT for testing using argument supplied attributes.
|
||||
*/
|
||||
public static String createIdToken(String provider, String clientId, String subject, String name, long expiry)
|
||||
{
|
||||
return "{" +
|
||||
"\"iss\": \"" + provider + "\"," +
|
||||
"\"sub\": \"" + subject + "\"," +
|
||||
"\"aud\": \"" + clientId + "\"," +
|
||||
"\"exp\": " + expiry + "," +
|
||||
"\"name\": \"" + name + "\"," +
|
||||
"\"email\": \"" + name + "@example.com" + "\"" +
|
||||
"}";
|
||||
}
|
||||
}
|
|
@ -1,402 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.tests.distribution.openid;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.security.openid.OpenIdConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.statistic.CounterStatistic;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenIdProvider extends ContainerLifeCycle
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class);
|
||||
|
||||
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
|
||||
private static final String AUTH_PATH = "/auth";
|
||||
private static final String TOKEN_PATH = "/token";
|
||||
private static final String END_SESSION_PATH = "/end_session";
|
||||
private final Map<String, User> issuedAuthCodes = new HashMap<>();
|
||||
|
||||
protected final String clientId;
|
||||
protected final String clientSecret;
|
||||
protected final List<String> redirectUris = new ArrayList<>();
|
||||
private final ServerConnector connector;
|
||||
private final Server server;
|
||||
private int port = 0;
|
||||
private String provider;
|
||||
private User preAuthedUser;
|
||||
private final CounterStatistic loggedInUsers = new CounterStatistic();
|
||||
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
String clientId = "CLIENT_ID123";
|
||||
String clientSecret = "PASSWORD123";
|
||||
int port = 5771;
|
||||
String redirectUri = "http://localhost:8080/j_security_check";
|
||||
|
||||
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
|
||||
openIdProvider.addRedirectUri(redirectUri);
|
||||
openIdProvider.setPort(port);
|
||||
openIdProvider.start();
|
||||
try
|
||||
{
|
||||
openIdProvider.join();
|
||||
}
|
||||
finally
|
||||
{
|
||||
openIdProvider.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public OpenIdProvider(String clientId, String clientSecret)
|
||||
{
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
ServletContextHandler contextHandler = new ServletContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH);
|
||||
server.setHandler(contextHandler);
|
||||
|
||||
addBean(server);
|
||||
}
|
||||
|
||||
public void setIdTokenDuration(long duration)
|
||||
{
|
||||
_idTokenDuration = duration;
|
||||
}
|
||||
|
||||
public long getIdTokenDuration()
|
||||
{
|
||||
return _idTokenDuration;
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException
|
||||
{
|
||||
server.join();
|
||||
}
|
||||
|
||||
public OpenIdConfiguration getOpenIdConfiguration()
|
||||
{
|
||||
String provider = getProvider();
|
||||
String authEndpoint = provider + AUTH_PATH;
|
||||
String tokenEndpoint = provider + TOKEN_PATH;
|
||||
return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
|
||||
}
|
||||
|
||||
public CounterStatistic getLoggedInUsers()
|
||||
{
|
||||
return loggedInUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
connector.setPort(port);
|
||||
super.doStart();
|
||||
provider = "http://localhost:" + connector.getLocalPort();
|
||||
}
|
||||
|
||||
public void setPort(int port)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void setUser(User user)
|
||||
{
|
||||
this.preAuthedUser = user;
|
||||
}
|
||||
|
||||
public String getProvider()
|
||||
{
|
||||
if (!isStarted() && port == 0)
|
||||
throw new IllegalStateException("Port of OpenIdProvider not configured");
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void addRedirectUri(String uri)
|
||||
{
|
||||
redirectUris.add(uri);
|
||||
}
|
||||
|
||||
public class AuthEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
if (!clientId.equals(req.getParameter("client_id")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id");
|
||||
return;
|
||||
}
|
||||
|
||||
String redirectUri = req.getParameter("redirect_uri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
LOG.warn("invalid redirectUri {}", redirectUri);
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String scopeString = req.getParameter("scope");
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
|
||||
if (!scopes.contains("openid"))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"code".equals(req.getParameter("response_type")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = req.getParameter("state");
|
||||
if (state == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
if (preAuthedUser == null)
|
||||
{
|
||||
PrintWriter writer = resp.getWriter();
|
||||
resp.setContentType("text/html");
|
||||
writer.println("<h2>Login to OpenID Connect Provider</h2>");
|
||||
writer.println("<form action=\"" + AUTH_PATH + "\" method=\"post\">");
|
||||
writer.println("<input type=\"text\" autocomplete=\"off\" placeholder=\"Username\" name=\"username\" required>");
|
||||
writer.println("<input type=\"hidden\" name=\"redirectUri\" value=\"" + redirectUri + "\">");
|
||||
writer.println("<input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
|
||||
writer.println("<input type=\"submit\">");
|
||||
writer.println("</form>");
|
||||
}
|
||||
else
|
||||
{
|
||||
redirectUser(resp, preAuthedUser, redirectUri, state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String redirectUri = req.getParameter("redirectUri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = req.getParameter("state");
|
||||
if (state == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
String username = req.getParameter("username");
|
||||
if (username == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = new User(username);
|
||||
redirectUser(resp, user, redirectUri, state);
|
||||
}
|
||||
|
||||
public void redirectUser(HttpServletResponse response, User user, String redirectUri, String state) throws IOException
|
||||
{
|
||||
String authCode = UUID.randomUUID().toString().replace("-", "");
|
||||
issuedAuthCodes.put(authCode, user);
|
||||
|
||||
try
|
||||
{
|
||||
redirectUri += "?code=" + authCode + "&state=" + state;
|
||||
response.sendRedirect(response.encodeRedirectURL(redirectUri));
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
issuedAuthCodes.remove(authCode);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String code = req.getParameter("code");
|
||||
|
||||
if (!clientId.equals(req.getParameter("client_id")) ||
|
||||
!clientSecret.equals(req.getParameter("client_secret")) ||
|
||||
!redirectUris.contains(req.getParameter("redirect_uri")) ||
|
||||
!"authorization_code".equals(req.getParameter("grant_type")) ||
|
||||
code == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = issuedAuthCodes.remove(code);
|
||||
if (user == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code");
|
||||
return;
|
||||
}
|
||||
|
||||
String accessToken = "ABCDEFG";
|
||||
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
|
||||
String response = "{" +
|
||||
"\"access_token\": \"" + accessToken + "\"," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
|
||||
"\"expires_in\": " + accessTokenDuration + "," +
|
||||
"\"token_type\": \"Bearer\"" +
|
||||
"}";
|
||||
|
||||
loggedInUsers.increment();
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().print(response);
|
||||
}
|
||||
}
|
||||
|
||||
private class EndSessionEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
doPost(req, resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String idToken = req.getParameter("id_token_hint");
|
||||
if (idToken == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint");
|
||||
return;
|
||||
}
|
||||
|
||||
String logoutRedirect = req.getParameter("post_logout_redirect_uri");
|
||||
if (logoutRedirect == null)
|
||||
{
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
resp.getWriter().println("logout success on end_session_endpoint");
|
||||
return;
|
||||
}
|
||||
|
||||
loggedInUsers.decrement();
|
||||
resp.setContentType("text/plain");
|
||||
resp.sendRedirect(logoutRedirect);
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String discoveryDocument = "{" +
|
||||
"\"issuer\": \"" + provider + "\"," +
|
||||
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
|
||||
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
|
||||
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
|
||||
"}";
|
||||
|
||||
resp.getWriter().write(discoveryDocument);
|
||||
}
|
||||
}
|
||||
|
||||
public static class User
|
||||
{
|
||||
private final String subject;
|
||||
private final String name;
|
||||
|
||||
public User(String name)
|
||||
{
|
||||
this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name);
|
||||
}
|
||||
|
||||
public User(String subject, String name)
|
||||
{
|
||||
this.subject = subject;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getSubject()
|
||||
{
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getIdToken(String provider, String clientId, long duration)
|
||||
{
|
||||
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (!(obj instanceof User))
|
||||
return false;
|
||||
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(subject, name);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,6 +55,11 @@
|
|||
<type>war</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-test-common</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-testers</artifactId>
|
||||
|
|
|
@ -17,8 +17,8 @@ import java.nio.file.Path;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.ee11.tests.distribution.openid.OpenIdProvider;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.tests.OpenIdProvider;
|
||||
import org.eclipse.jetty.tests.distribution.AbstractJettyHomeTest;
|
||||
import org.eclipse.jetty.tests.testers.JettyHomeTester;
|
||||
import org.eclipse.jetty.tests.testers.Tester;
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee11.tests.distribution.openid;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* A basic JWT encoder for testing purposes.
|
||||
*/
|
||||
public class JwtEncoder
|
||||
{
|
||||
private static final Base64.Encoder ENCODER = Base64.getUrlEncoder();
|
||||
private static final String DEFAULT_HEADER = "{\"INFO\": \"this is not used or checked in our implementation\"}";
|
||||
private static final String DEFAULT_SIGNATURE = "we do not validate signature as we use the authorization code flow";
|
||||
|
||||
public static String encode(String idToken)
|
||||
{
|
||||
return stripPadding(ENCODER.encodeToString(DEFAULT_HEADER.getBytes())) + "." +
|
||||
stripPadding(ENCODER.encodeToString(idToken.getBytes())) + "." +
|
||||
stripPadding(ENCODER.encodeToString(DEFAULT_SIGNATURE.getBytes()));
|
||||
}
|
||||
|
||||
private static String stripPadding(String paddedBase64)
|
||||
{
|
||||
return paddedBase64.split("=")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic JWT for testing using argument supplied attributes.
|
||||
*/
|
||||
public static String createIdToken(String provider, String clientId, String subject, String name, long expiry)
|
||||
{
|
||||
return "{" +
|
||||
"\"iss\": \"" + provider + "\"," +
|
||||
"\"sub\": \"" + subject + "\"," +
|
||||
"\"aud\": \"" + clientId + "\"," +
|
||||
"\"exp\": " + expiry + "," +
|
||||
"\"name\": \"" + name + "\"," +
|
||||
"\"email\": \"" + name + "@example.com" + "\"" +
|
||||
"}";
|
||||
}
|
||||
}
|
|
@ -1,402 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee11.tests.distribution.openid;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee11.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.security.openid.OpenIdConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.statistic.CounterStatistic;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenIdProvider extends ContainerLifeCycle
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class);
|
||||
|
||||
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
|
||||
private static final String AUTH_PATH = "/auth";
|
||||
private static final String TOKEN_PATH = "/token";
|
||||
private static final String END_SESSION_PATH = "/end_session";
|
||||
private final Map<String, User> issuedAuthCodes = new HashMap<>();
|
||||
|
||||
protected final String clientId;
|
||||
protected final String clientSecret;
|
||||
protected final List<String> redirectUris = new ArrayList<>();
|
||||
private final ServerConnector connector;
|
||||
private final Server server;
|
||||
private int port = 0;
|
||||
private String provider;
|
||||
private User preAuthedUser;
|
||||
private final CounterStatistic loggedInUsers = new CounterStatistic();
|
||||
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
String clientId = "CLIENT_ID123";
|
||||
String clientSecret = "PASSWORD123";
|
||||
int port = 5771;
|
||||
String redirectUri = "http://localhost:8080/j_security_check";
|
||||
|
||||
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
|
||||
openIdProvider.addRedirectUri(redirectUri);
|
||||
openIdProvider.setPort(port);
|
||||
openIdProvider.start();
|
||||
try
|
||||
{
|
||||
openIdProvider.join();
|
||||
}
|
||||
finally
|
||||
{
|
||||
openIdProvider.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public OpenIdProvider(String clientId, String clientSecret)
|
||||
{
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
ServletContextHandler contextHandler = new ServletContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH);
|
||||
server.setHandler(contextHandler);
|
||||
|
||||
addBean(server);
|
||||
}
|
||||
|
||||
public void setIdTokenDuration(long duration)
|
||||
{
|
||||
_idTokenDuration = duration;
|
||||
}
|
||||
|
||||
public long getIdTokenDuration()
|
||||
{
|
||||
return _idTokenDuration;
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException
|
||||
{
|
||||
server.join();
|
||||
}
|
||||
|
||||
public OpenIdConfiguration getOpenIdConfiguration()
|
||||
{
|
||||
String provider = getProvider();
|
||||
String authEndpoint = provider + AUTH_PATH;
|
||||
String tokenEndpoint = provider + TOKEN_PATH;
|
||||
return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
|
||||
}
|
||||
|
||||
public CounterStatistic getLoggedInUsers()
|
||||
{
|
||||
return loggedInUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
connector.setPort(port);
|
||||
super.doStart();
|
||||
provider = "http://localhost:" + connector.getLocalPort();
|
||||
}
|
||||
|
||||
public void setPort(int port)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void setUser(User user)
|
||||
{
|
||||
this.preAuthedUser = user;
|
||||
}
|
||||
|
||||
public String getProvider()
|
||||
{
|
||||
if (!isStarted() && port == 0)
|
||||
throw new IllegalStateException("Port of OpenIdProvider not configured");
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void addRedirectUri(String uri)
|
||||
{
|
||||
redirectUris.add(uri);
|
||||
}
|
||||
|
||||
public class AuthEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
if (!clientId.equals(req.getParameter("client_id")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id");
|
||||
return;
|
||||
}
|
||||
|
||||
String redirectUri = req.getParameter("redirect_uri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
LOG.warn("invalid redirectUri {}", redirectUri);
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String scopeString = req.getParameter("scope");
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
|
||||
if (!scopes.contains("openid"))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"code".equals(req.getParameter("response_type")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = req.getParameter("state");
|
||||
if (state == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
if (preAuthedUser == null)
|
||||
{
|
||||
PrintWriter writer = resp.getWriter();
|
||||
resp.setContentType("text/html");
|
||||
writer.println("<h2>Login to OpenID Connect Provider</h2>");
|
||||
writer.println("<form action=\"" + AUTH_PATH + "\" method=\"post\">");
|
||||
writer.println("<input type=\"text\" autocomplete=\"off\" placeholder=\"Username\" name=\"username\" required>");
|
||||
writer.println("<input type=\"hidden\" name=\"redirectUri\" value=\"" + redirectUri + "\">");
|
||||
writer.println("<input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
|
||||
writer.println("<input type=\"submit\">");
|
||||
writer.println("</form>");
|
||||
}
|
||||
else
|
||||
{
|
||||
redirectUser(resp, preAuthedUser, redirectUri, state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String redirectUri = req.getParameter("redirectUri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = req.getParameter("state");
|
||||
if (state == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
String username = req.getParameter("username");
|
||||
if (username == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = new User(username);
|
||||
redirectUser(resp, user, redirectUri, state);
|
||||
}
|
||||
|
||||
public void redirectUser(HttpServletResponse response, User user, String redirectUri, String state) throws IOException
|
||||
{
|
||||
String authCode = UUID.randomUUID().toString().replace("-", "");
|
||||
issuedAuthCodes.put(authCode, user);
|
||||
|
||||
try
|
||||
{
|
||||
redirectUri += "?code=" + authCode + "&state=" + state;
|
||||
response.sendRedirect(response.encodeRedirectURL(redirectUri));
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
issuedAuthCodes.remove(authCode);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String code = req.getParameter("code");
|
||||
|
||||
if (!clientId.equals(req.getParameter("client_id")) ||
|
||||
!clientSecret.equals(req.getParameter("client_secret")) ||
|
||||
!redirectUris.contains(req.getParameter("redirect_uri")) ||
|
||||
!"authorization_code".equals(req.getParameter("grant_type")) ||
|
||||
code == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = issuedAuthCodes.remove(code);
|
||||
if (user == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code");
|
||||
return;
|
||||
}
|
||||
|
||||
String accessToken = "ABCDEFG";
|
||||
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
|
||||
String response = "{" +
|
||||
"\"access_token\": \"" + accessToken + "\"," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
|
||||
"\"expires_in\": " + accessTokenDuration + "," +
|
||||
"\"token_type\": \"Bearer\"" +
|
||||
"}";
|
||||
|
||||
loggedInUsers.increment();
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().print(response);
|
||||
}
|
||||
}
|
||||
|
||||
private class EndSessionEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
doPost(req, resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String idToken = req.getParameter("id_token_hint");
|
||||
if (idToken == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint");
|
||||
return;
|
||||
}
|
||||
|
||||
String logoutRedirect = req.getParameter("post_logout_redirect_uri");
|
||||
if (logoutRedirect == null)
|
||||
{
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
resp.getWriter().println("logout success on end_session_endpoint");
|
||||
return;
|
||||
}
|
||||
|
||||
loggedInUsers.decrement();
|
||||
resp.setContentType("text/plain");
|
||||
resp.sendRedirect(logoutRedirect);
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String discoveryDocument = "{" +
|
||||
"\"issuer\": \"" + provider + "\"," +
|
||||
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
|
||||
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
|
||||
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
|
||||
"}";
|
||||
|
||||
resp.getWriter().write(discoveryDocument);
|
||||
}
|
||||
}
|
||||
|
||||
public static class User
|
||||
{
|
||||
private final String subject;
|
||||
private final String name;
|
||||
|
||||
public User(String name)
|
||||
{
|
||||
this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name);
|
||||
}
|
||||
|
||||
public User(String subject, String name)
|
||||
{
|
||||
this.subject = subject;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getSubject()
|
||||
{
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getIdToken(String provider, String clientId, long duration)
|
||||
{
|
||||
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (!(obj instanceof User))
|
||||
return false;
|
||||
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(subject, name);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -56,6 +56,11 @@
|
|||
<type>war</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-test-common</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-testers</artifactId>
|
||||
|
|
|
@ -17,8 +17,8 @@ import java.nio.file.Path;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.ee9.tests.distribution.openid.OpenIdProvider;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.tests.OpenIdProvider;
|
||||
import org.eclipse.jetty.tests.distribution.AbstractJettyHomeTest;
|
||||
import org.eclipse.jetty.tests.testers.JettyHomeTester;
|
||||
import org.eclipse.jetty.tests.testers.Tester;
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.tests.distribution.openid;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* A basic JWT encoder for testing purposes.
|
||||
*/
|
||||
public class JwtEncoder
|
||||
{
|
||||
private static final Base64.Encoder ENCODER = Base64.getUrlEncoder();
|
||||
private static final String DEFAULT_HEADER = "{\"INFO\": \"this is not used or checked in our implementation\"}";
|
||||
private static final String DEFAULT_SIGNATURE = "we do not validate signature as we use the authorization code flow";
|
||||
|
||||
public static String encode(String idToken)
|
||||
{
|
||||
return stripPadding(ENCODER.encodeToString(DEFAULT_HEADER.getBytes())) + "." +
|
||||
stripPadding(ENCODER.encodeToString(idToken.getBytes())) + "." +
|
||||
stripPadding(ENCODER.encodeToString(DEFAULT_SIGNATURE.getBytes()));
|
||||
}
|
||||
|
||||
private static String stripPadding(String paddedBase64)
|
||||
{
|
||||
return paddedBase64.split("=")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic JWT for testing using argument supplied attributes.
|
||||
*/
|
||||
public static String createIdToken(String provider, String clientId, String subject, String name, long expiry)
|
||||
{
|
||||
return "{" +
|
||||
"\"iss\": \"" + provider + "\"," +
|
||||
"\"sub\": \"" + subject + "\"," +
|
||||
"\"aud\": \"" + clientId + "\"," +
|
||||
"\"exp\": " + expiry + "," +
|
||||
"\"name\": \"" + name + "\"," +
|
||||
"\"email\": \"" + name + "@example.com" + "\"" +
|
||||
"}";
|
||||
}
|
||||
}
|
|
@ -1,402 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.tests.distribution.openid;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee9.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.security.openid.OpenIdConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.statistic.CounterStatistic;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenIdProvider extends ContainerLifeCycle
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class);
|
||||
|
||||
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
|
||||
private static final String AUTH_PATH = "/auth";
|
||||
private static final String TOKEN_PATH = "/token";
|
||||
private static final String END_SESSION_PATH = "/end_session";
|
||||
private final Map<String, User> issuedAuthCodes = new HashMap<>();
|
||||
|
||||
protected final String clientId;
|
||||
protected final String clientSecret;
|
||||
protected final List<String> redirectUris = new ArrayList<>();
|
||||
private final ServerConnector connector;
|
||||
private final Server server;
|
||||
private int port = 0;
|
||||
private String provider;
|
||||
private User preAuthedUser;
|
||||
private final CounterStatistic loggedInUsers = new CounterStatistic();
|
||||
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
String clientId = "CLIENT_ID123";
|
||||
String clientSecret = "PASSWORD123";
|
||||
int port = 5771;
|
||||
String redirectUri = "http://localhost:8080/j_security_check";
|
||||
|
||||
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
|
||||
openIdProvider.addRedirectUri(redirectUri);
|
||||
openIdProvider.setPort(port);
|
||||
openIdProvider.start();
|
||||
try
|
||||
{
|
||||
openIdProvider.join();
|
||||
}
|
||||
finally
|
||||
{
|
||||
openIdProvider.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public OpenIdProvider(String clientId, String clientSecret)
|
||||
{
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
ServletContextHandler contextHandler = new ServletContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH);
|
||||
server.setHandler(contextHandler);
|
||||
|
||||
addBean(server);
|
||||
}
|
||||
|
||||
public void setIdTokenDuration(long duration)
|
||||
{
|
||||
_idTokenDuration = duration;
|
||||
}
|
||||
|
||||
public long getIdTokenDuration()
|
||||
{
|
||||
return _idTokenDuration;
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException
|
||||
{
|
||||
server.join();
|
||||
}
|
||||
|
||||
public OpenIdConfiguration getOpenIdConfiguration()
|
||||
{
|
||||
String provider = getProvider();
|
||||
String authEndpoint = provider + AUTH_PATH;
|
||||
String tokenEndpoint = provider + TOKEN_PATH;
|
||||
return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
|
||||
}
|
||||
|
||||
public CounterStatistic getLoggedInUsers()
|
||||
{
|
||||
return loggedInUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
connector.setPort(port);
|
||||
super.doStart();
|
||||
provider = "http://localhost:" + connector.getLocalPort();
|
||||
}
|
||||
|
||||
public void setPort(int port)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void setUser(User user)
|
||||
{
|
||||
this.preAuthedUser = user;
|
||||
}
|
||||
|
||||
public String getProvider()
|
||||
{
|
||||
if (!isStarted() && port == 0)
|
||||
throw new IllegalStateException("Port of OpenIdProvider not configured");
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void addRedirectUri(String uri)
|
||||
{
|
||||
redirectUris.add(uri);
|
||||
}
|
||||
|
||||
public class AuthEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
if (!clientId.equals(req.getParameter("client_id")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id");
|
||||
return;
|
||||
}
|
||||
|
||||
String redirectUri = req.getParameter("redirect_uri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
LOG.warn("invalid redirectUri {}", redirectUri);
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String scopeString = req.getParameter("scope");
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
|
||||
if (!scopes.contains("openid"))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"code".equals(req.getParameter("response_type")))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = req.getParameter("state");
|
||||
if (state == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
if (preAuthedUser == null)
|
||||
{
|
||||
PrintWriter writer = resp.getWriter();
|
||||
resp.setContentType("text/html");
|
||||
writer.println("<h2>Login to OpenID Connect Provider</h2>");
|
||||
writer.println("<form action=\"" + AUTH_PATH + "\" method=\"post\">");
|
||||
writer.println("<input type=\"text\" autocomplete=\"off\" placeholder=\"Username\" name=\"username\" required>");
|
||||
writer.println("<input type=\"hidden\" name=\"redirectUri\" value=\"" + redirectUri + "\">");
|
||||
writer.println("<input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
|
||||
writer.println("<input type=\"submit\">");
|
||||
writer.println("</form>");
|
||||
}
|
||||
else
|
||||
{
|
||||
redirectUser(resp, preAuthedUser, redirectUri, state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String redirectUri = req.getParameter("redirectUri");
|
||||
if (!redirectUris.contains(redirectUri))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
|
||||
return;
|
||||
}
|
||||
|
||||
String state = req.getParameter("state");
|
||||
if (state == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
|
||||
return;
|
||||
}
|
||||
|
||||
String username = req.getParameter("username");
|
||||
if (username == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = new User(username);
|
||||
redirectUser(resp, user, redirectUri, state);
|
||||
}
|
||||
|
||||
public void redirectUser(HttpServletResponse response, User user, String redirectUri, String state) throws IOException
|
||||
{
|
||||
String authCode = UUID.randomUUID().toString().replace("-", "");
|
||||
issuedAuthCodes.put(authCode, user);
|
||||
|
||||
try
|
||||
{
|
||||
redirectUri += "?code=" + authCode + "&state=" + state;
|
||||
response.sendRedirect(response.encodeRedirectURL(redirectUri));
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
issuedAuthCodes.remove(authCode);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String code = req.getParameter("code");
|
||||
|
||||
if (!clientId.equals(req.getParameter("client_id")) ||
|
||||
!clientSecret.equals(req.getParameter("client_secret")) ||
|
||||
!redirectUris.contains(req.getParameter("redirect_uri")) ||
|
||||
!"authorization_code".equals(req.getParameter("grant_type")) ||
|
||||
code == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request");
|
||||
return;
|
||||
}
|
||||
|
||||
User user = issuedAuthCodes.remove(code);
|
||||
if (user == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code");
|
||||
return;
|
||||
}
|
||||
|
||||
String accessToken = "ABCDEFG";
|
||||
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
|
||||
String response = "{" +
|
||||
"\"access_token\": \"" + accessToken + "\"," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
|
||||
"\"expires_in\": " + accessTokenDuration + "," +
|
||||
"\"token_type\": \"Bearer\"" +
|
||||
"}";
|
||||
|
||||
loggedInUsers.increment();
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().print(response);
|
||||
}
|
||||
}
|
||||
|
||||
private class EndSessionEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
doPost(req, resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String idToken = req.getParameter("id_token_hint");
|
||||
if (idToken == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint");
|
||||
return;
|
||||
}
|
||||
|
||||
String logoutRedirect = req.getParameter("post_logout_redirect_uri");
|
||||
if (logoutRedirect == null)
|
||||
{
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
resp.getWriter().println("logout success on end_session_endpoint");
|
||||
return;
|
||||
}
|
||||
|
||||
loggedInUsers.decrement();
|
||||
resp.setContentType("text/plain");
|
||||
resp.sendRedirect(logoutRedirect);
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String discoveryDocument = "{" +
|
||||
"\"issuer\": \"" + provider + "\"," +
|
||||
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
|
||||
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
|
||||
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
|
||||
"}";
|
||||
|
||||
resp.getWriter().write(discoveryDocument);
|
||||
}
|
||||
}
|
||||
|
||||
public static class User
|
||||
{
|
||||
private final String subject;
|
||||
private final String name;
|
||||
|
||||
public User(String name)
|
||||
{
|
||||
this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name);
|
||||
}
|
||||
|
||||
public User(String subject, String name)
|
||||
{
|
||||
this.subject = subject;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getSubject()
|
||||
{
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getIdToken(String provider, String clientId, long duration)
|
||||
{
|
||||
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (!(obj instanceof User))
|
||||
return false;
|
||||
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(subject, name);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue