Jetty 12.0.x core security (#9405)

core security module

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: gregw <gregw@webtide.com>
Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
Co-authored-by: Lachlan Roberts <lachlan@webtide.com>
Co-authored-by: Jan Bartel <janb@webtide.com>
Co-authored-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Greg Wilkins 2023-05-02 15:35:49 +02:00 committed by GitHub
parent 379de19e5c
commit 7275bf15a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
434 changed files with 8659 additions and 19877 deletions

View File

@ -70,11 +70,11 @@ embedded, via the `jetty.xml` or in a context file for the webapp.
This is what the configuration within a context XML file would look like:
[source, xml, subs="{sub-order}"]
[source,xml,subs="{sub-order}"]
----
<Get name="securityHandler">
<Set name="loginService">
<New class="org.eclipse.jetty.security.ConfigurableSpnegoLoginService">
<New class="org.eclipse.jetty.security.SPNEGOLoginService">
<Arg>Test Realm</Arg>
<Arg><Ref refid="authorizationService" /></Arg>
<Set name="keyTabPath"><Ref refid="keyTabPath" /></Set>

View File

@ -56,7 +56,7 @@ See more about the contents of this file in the xref:og-jaas-loginconf[Configuri
[[og-jaas-webapp]]
===== Configure the webapp for JAAS
The `<realm-name>` in `web.xml` will be used to identify the `org.eclipse.jetty.jaas.JAASLoginService` declaration that integrates JAAS with Jetty.
The `<realm-name>` in `web.xml` will be used to identify the `org.eclipse.jetty.security.jaas.JAASLoginService` declaration that integrates JAAS with Jetty.
For example, this `web.xml` contains a realm called `Test JAAS Realm`:
@ -71,14 +71,14 @@ For example, this `web.xml` contains a realm called `Test JAAS Realm`:
</form-login-config>
</login-config>
----
<1> The name of the realm, which must be _identical_ to the name of an `org.eclipse.jetty.jaas.JAASLoginService` declaration.
<1> The name of the realm, which must be _identical_ to the name of an `org.eclipse.jetty.security.jaas.JAASLoginService` declaration.
We now need to declare an `org.eclipse.jetty.jaas.JAASLoginService` that references the realm name of `Test JAAS Realm`.
We now need to declare an `org.eclipse.jetty.security.jaas.JAASLoginService` that references the realm name of `Test JAAS Realm`.
Here's an example of a suitable XML snippet:
[source,xml,subs=verbatim]
----
<New class="org.eclipse.jetty.jaas.JAASLoginService">
<New class="org.eclipse.jetty.security.jaas.JAASLoginService">
<Set name="Name">Test JAAS Realm</Set> <!--1-->
<Set name="LoginModuleName">xyz</Set> <!--2-->
</New>
@ -86,9 +86,9 @@ Here's an example of a suitable XML snippet:
<1> The name is the _same_ as that declared in the `<realm-name>` in `web.xml`.
<2> The name that identifies a set of `javax.security.auth.spi.LoginModule` configurations that comprise the xref:og-jaas-loginconf[JAAS config file] identified in the `jetty.jaas.login.conf` property of the xref:og-jaas-module[`jaas` module].
The `org.eclipse.jetty.jaas.JAASLoginService` can be declared in a couple of different places, pick whichever suits your purposes best:
The `org.eclipse.jetty.security.jaas.JAASLoginService` can be declared in a couple of different places, pick whichever suits your purposes best:
* If you have more than one webapp that you would like to use the same security infrastructure, then you can declare your `org.eclipse.jetty.jaas.JAASLoginService` as a bean that is added to the `org.eclipse.jetty.server.Server`.
* If you have more than one webapp that you would like to use the same security infrastructure, then you can declare your `org.eclipse.jetty.security.jaas.JAASLoginService` as a bean that is added to the `org.eclipse.jetty.server.Server`.
The file in which you declare this needs to be on Jetty's execution path.
The recommended procedure is to create a file in your `$jetty.base/etc` directory and then ensure it is on the classpath either by adding it to the Jetty xref:og-start-jar[start command line], or more conveniently to a xref:custom-modules[custom module].
+
@ -101,7 +101,7 @@ Here's an example of this type of XML file:
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addBean">
<Arg>
<New class="org.eclipse.jetty.jaas.JAASLoginService">
<New class="org.eclipse.jetty.security.jaas.JAASLoginService">
<Set name="name">Test JAAS Realm</Set>
<Set name="LoginModuleName">xyz</Set>
</New>
@ -110,7 +110,7 @@ Here's an example of this type of XML file:
</Configure>
----
* Alternatively, if you want to use JAAS with a specific webapp only, you declare your `org.eclipse.jetty.jaas.JAASLoginService` in a context XLM file specific to that webapp:
* Alternatively, if you want to use JAAS with a specific webapp only, you declare your `org.eclipse.jetty.security.jaas.JAASLoginService` in a context XLM file specific to that webapp:
+
[source,xml]
----
@ -120,7 +120,7 @@ Here's an example of this type of XML file:
<Set name="securityHandler">
<New class="org.eclipse.jetty.security.ConstraintSecurityHandler">
<Set name="loginService">
<New class="org.eclipse.jetty.jaas.JAASLoginService">
<New class="org.eclipse.jetty.security.jaas.JAASLoginService">
<Set name="name">Test JAAS Realm</Set>
<Set name="loginModuleName">xyz</Set>
</New>
@ -145,7 +145,7 @@ xyz { <1>
com.other.OtherLoginModule optional; <3>
};
----
<1> The name of the configuration _exactly_ as specified in your `org.eclipse.jetty.jaas.JAASLoginService` declaration.
<1> The name of the configuration _exactly_ as specified in your `org.eclipse.jetty.security.jaas.JAASLoginService` declaration.
<2> The first `LoginModule` declaration, containing the classname of the `LoginModule` and its configuration properties.
<3> A second `LoginModule` declaration.
You can provide as many `LoginModule` alternatives as you like, with a minimum of one.
@ -154,10 +154,10 @@ Refer to the link:https://docs.oracle.com/javase/7/docs/api/javax/security/auth/
[[og-jaas-loginmodules]]
==== Provided LoginModules
* link:{javadoc-url}/org/eclipse/jetty/jaas/spi/JDBCLoginModule.html[`org.eclipse.jetty.jaas.spi.JDBCLoginModule`]
* link:{javadoc-url}/org/eclipse/jetty/jaas/spi/PropertyFileLoginModule.html[`org.eclipse.jetty.jaas.spi.PropertyFileLoginModule`]
* link:{javadoc-url}/org/eclipse/jetty/jaas/spi/DataSourceLoginModule.html[`org.eclipse.jetty.jaas.spi.DataSourceLoginModule`]
* link:{javadoc-url}/org/eclipse/jetty/jaas/spi/LdapLoginModule.html[`org.eclipse.jetty.jaas.ldap.LdapLoginModule`]
* link:{javadoc-url}/org/eclipse/jetty/security/jaas/spi/JDBCLoginModule.html[`org.eclipse.jetty.security.jaas.spi.JDBCLoginModule`]
* link:{javadoc-url}/org/eclipse/jetty/security/jaas/spi/PropertyFileLoginModule.html[`org.eclipse.jetty.security.jaas.spi.PropertyFileLoginModule`]
* link:{javadoc-url}/org/eclipse/jetty/security/jaas/spi/DataSourceLoginModule.html[`org.eclipse.jetty.security.jaas.spi.DataSourceLoginModule`]
* link:{javadoc-url}/org/eclipse/jetty/security/jaas/spi/LdapLoginModule.html[`org.eclipse.jetty.security.jaas.ldap.LdapLoginModule`]
[NOTE]
====
@ -167,7 +167,7 @@ The class link:{javadoc-url}/org/eclipse/jetty/util/security/Password.html[`org.
===== JDBCLoginModule
The `org.eclipse.jetty.jaas.spi.JDBCLoginModule` stores user passwords and roles in a database accessed via JDBC calls.
The `org.eclipse.jetty.security.jaas.spi.JDBCLoginModule` stores user passwords and roles in a database accessed via JDBC calls.
You can configure the JDBC connection information, as well as the names of the table and columns storing the username and credential, and the names of the table and columns storing the roles.
Here is an example xref:og-jaas-loginconf[login module configuration file] entry for it using an HSQLDB driver:
@ -175,7 +175,7 @@ Here is an example xref:og-jaas-loginconf[login module configuration file] entry
[source,subs=verbatim]
----
jdbc { <1>
org.eclipse.jetty.jaas.spi.JDBCLoginModule required <2><3>
org.eclipse.jetty.security.jaas.spi.JDBCLoginModule required <2><3>
dbUrl="jdbc:hsqldb:." <4>
dbUserName="sa" <5>
dbDriver="org.hsqldb.jdbcDriver" <6>
@ -216,7 +216,7 @@ Note that passwords can be stored in the database in plain text or encoded forma
===== DataSourceLoginModule
Similar to the `org.eclipse.jetty.jaas.spi.JDBCLoginModule`, but using a `javax.sql.DataSource` to connect to the database instead of a JDBC driver.
Similar to the `org.eclipse.jetty.security.jaas.spi.JDBCLoginModule`, but using a `javax.sql.DataSource` to connect to the database instead of a JDBC driver.
The `javax.sql.DataSource` is obtained at runtime by performing a JNDI lookup on `java:comp/env/${dnJNDIName}`.
A sample login module configuration for this `LoginModule`:
@ -224,7 +224,7 @@ A sample login module configuration for this `LoginModule`:
[source,subs=verbatim]
----
ds { <1>
org.eclipse.jetty.jaas.spi.DataSourceLoginModule required <2><3>
org.eclipse.jetty.security.jaas.spi.DataSourceLoginModule required <2><3>
dbJNDIName="ds" <4>
userTable="myusers" <5>
userField="myuser" <6>
@ -252,7 +252,7 @@ With this login module implementation, the authentication and role information i
[source,subs=verbatim]
----
props { <1>
org.eclipse.jetty.jaas.spi.PropertyFileLoginModule required <2><3>
org.eclipse.jetty.security.jaas.spi.PropertyFileLoginModule required <2><3>
file="/somewhere/somefile.props"; <4>
};
----
@ -281,7 +281,7 @@ The contents of the file are fully read in and cached in memory the first time a
===== LdapLoginModule
The `org.eclipse.jetty.jaas.spi.LdapLoginModule` uses LDAP to access authentication and authorization information stored in a directory.
The `org.eclipse.jetty.security.jaas.spi.LdapLoginModule` uses LDAP to access authentication and authorization information stored in a directory.
The LDAP connection information and structure of the authentication/authorization data can be configured.
Here's an example:
@ -289,7 +289,7 @@ Here's an example:
[source,subs=verbatim]
----
example { <1>
org.eclipse.jetty.jaas.spi.LdapLoginModule required <2><3>
org.eclipse.jetty.security.jaas.spi.LdapLoginModule required <2><3>
contextFactory="com.sun.jndi.ldap.LdapCtxFactory" <4>
hostname="ldap.example.com" <5>
port="389" <6>

File diff suppressed because it is too large Load Diff

View File

@ -267,16 +267,11 @@
<artifactId>jetty-http3-server</artifactId>
<scope>provided</scope>
</dependency>
<!--<dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jaas</artifactId>
<artifactId>jetty-security</artifactId>
<scope>provided</scope>
</dependency>-->
<!--<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jaspi</artifactId>
<scope>provided</scope>
</dependency>-->
</dependency>
<dependency>
<groupId>org.eclipse.jetty.memcached</groupId>
<artifactId>jetty-memcached-sessions</artifactId>

View File

@ -117,6 +117,16 @@
<artifactId>jetty-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-session</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.client;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.function.BiFunction;
import org.eclipse.jetty.util.component.Dumpable;
import org.slf4j.Logger;
@ -35,25 +36,58 @@ public class RequestListeners implements Dumpable
private Request.SuccessListener successListener;
private Request.FailureListener failureListener;
public void addListener(Request.Listener listener)
public boolean addListener(Request.Listener listener)
{
addQueuedListener(listener);
addBeginListener(listener);
addHeadersListener(listener);
addCommitListener(listener);
addContentListener(listener);
addSuccessListener(listener);
addFailureListener(listener);
// Use binary OR to avoid short-circuit.
return addQueuedListener(listener) |
addBeginListener(listener) |
addHeadersListener(listener) |
addCommitListener(listener) |
addContentListener(listener) |
addSuccessListener(listener) |
addFailureListener(listener);
}
public void addQueuedListener(Request.QueuedListener listener)
public boolean removeListener(Request.Listener listener)
{
// Use binary OR to avoid short-circuit.
return removeQueuedListener(listener) |
removeBeginListener(listener) |
removeHeadersListener(listener) |
removeCommitListener(listener) |
removeContentListener(listener) |
removeSuccessListener(listener) |
removeFailureListener(listener);
}
public boolean addQueuedListener(Request.QueuedListener listener)
{
if (listener == null)
return false;
Request.QueuedListener existing = queuedListener;
queuedListener = existing == null ? listener : request ->
queuedListener = existing == null ? listener : new QueuedListenerLink(existing, listener);
return true;
}
public boolean removeQueuedListener(Request.QueuedListener listener)
{
if (listener == null)
return false;
if (queuedListener == listener)
{
notifyQueued(existing, request);
notifyQueued(listener, request);
};
queuedListener = null;
return true;
}
if (queuedListener instanceof QueuedListenerLink link)
{
Request.QueuedListener remaining = link.remove(listener);
if (remaining != null)
{
queuedListener = remaining;
return true;
}
}
return false;
}
protected static void notifyQueued(Request.QueuedListener listener, Request request)
@ -69,14 +103,34 @@ public class RequestListeners implements Dumpable
}
}
public void addBeginListener(Request.BeginListener listener)
public boolean addBeginListener(Request.BeginListener listener)
{
if (listener == null)
return false;
Request.BeginListener existing = beginListener;
beginListener = existing == null ? listener : request ->
beginListener = existing == null ? listener : new BeginListenerLink(existing, listener);
return true;
}
public boolean removeBeginListener(Request.BeginListener listener)
{
if (listener == null)
return false;
if (beginListener == listener)
{
notifyBegin(existing, request);
notifyBegin(listener, request);
};
beginListener = null;
return true;
}
if (beginListener instanceof BeginListenerLink link)
{
Request.BeginListener remaining = link.remove(listener);
if (remaining != null)
{
beginListener = remaining;
return true;
}
}
return false;
}
protected static void notifyBegin(Request.BeginListener listener, Request request)
@ -92,14 +146,34 @@ public class RequestListeners implements Dumpable
}
}
public void addHeadersListener(Request.HeadersListener listener)
public boolean addHeadersListener(Request.HeadersListener listener)
{
if (listener == null)
return false;
Request.HeadersListener existing = headersListener;
headersListener = existing == null ? listener : request ->
headersListener = existing == null ? listener : new HeadersListenerLink(existing, listener);
return true;
}
public boolean removeHeadersListener(Request.HeadersListener listener)
{
if (listener == null)
return false;
if (headersListener == listener)
{
notifyHeaders(existing, request);
notifyHeaders(listener, request);
};
headersListener = null;
return true;
}
if (headersListener instanceof HeadersListenerLink link)
{
Request.HeadersListener remaining = link.remove(listener);
if (remaining != null)
{
headersListener = remaining;
return true;
}
}
return false;
}
protected static void notifyHeaders(Request.HeadersListener listener, Request request)
@ -115,14 +189,34 @@ public class RequestListeners implements Dumpable
}
}
public void addCommitListener(Request.CommitListener listener)
public boolean addCommitListener(Request.CommitListener listener)
{
if (listener == null)
return false;
Request.CommitListener existing = commitListener;
commitListener = existing == null ? listener : request ->
commitListener = existing == null ? listener : new CommitListenerLink(existing, listener);
return true;
}
public boolean removeCommitListener(Request.CommitListener listener)
{
if (listener == null)
return false;
if (commitListener == listener)
{
notifyCommit(existing, request);
notifyCommit(listener, request);
};
commitListener = null;
return true;
}
if (commitListener instanceof CommitListenerLink link)
{
Request.CommitListener remaining = link.remove(listener);
if (remaining != null)
{
commitListener = remaining;
return true;
}
}
return false;
}
protected static void notifyCommit(Request.CommitListener listener, Request request)
@ -138,14 +232,34 @@ public class RequestListeners implements Dumpable
}
}
public void addContentListener(Request.ContentListener listener)
public boolean addContentListener(Request.ContentListener listener)
{
if (listener == null)
return false;
Request.ContentListener existing = contentListener;
contentListener = existing == null ? listener : (request, byteBuffer) ->
contentListener = existing == null ? listener : new ContentListenerLink(existing, listener);
return true;
}
public boolean removeContentListener(Request.ContentListener listener)
{
if (listener == null)
return false;
if (contentListener == listener)
{
notifyContent(existing, request, byteBuffer);
notifyContent(listener, request, byteBuffer);
};
contentListener = null;
return true;
}
if (contentListener instanceof ContentListenerLink link)
{
Request.ContentListener remaining = link.remove(listener);
if (remaining != null)
{
contentListener = remaining;
return true;
}
}
return false;
}
protected static void notifyContent(Request.ContentListener listener, Request request, ByteBuffer byteBuffer)
@ -166,14 +280,34 @@ public class RequestListeners implements Dumpable
}
}
public void addSuccessListener(Request.SuccessListener listener)
public boolean addSuccessListener(Request.SuccessListener listener)
{
if (listener == null)
return false;
Request.SuccessListener existing = successListener;
successListener = existing == null ? listener : request ->
successListener = existing == null ? listener : new SuccessListenerLink(existing, listener);
return true;
}
public boolean removeSuccessListener(Request.SuccessListener listener)
{
if (listener == null)
return false;
if (successListener == listener)
{
notifySuccess(existing, request);
notifySuccess(listener, request);
};
successListener = null;
return true;
}
if (successListener instanceof SuccessListenerLink link)
{
Request.SuccessListener remaining = link.remove(listener);
if (remaining != null)
{
successListener = remaining;
return true;
}
}
return false;
}
protected static void notifySuccess(Request.SuccessListener listener, Request request)
@ -189,14 +323,34 @@ public class RequestListeners implements Dumpable
}
}
public void addFailureListener(Request.FailureListener listener)
public boolean addFailureListener(Request.FailureListener listener)
{
if (listener == null)
return false;
Request.FailureListener existing = failureListener;
failureListener = existing == null ? listener : (request, failure) ->
failureListener = existing == null ? listener : new FailureListenerLink(existing, listener);
return true;
}
public boolean removeFailureListener(Request.FailureListener listener)
{
if (listener == null)
return false;
if (failureListener == listener)
{
notifyFailure(existing, request, failure);
notifyFailure(listener, request, failure);
};
failureListener = null;
return true;
}
if (failureListener instanceof FailureListenerLink link)
{
Request.FailureListener remaining = link.remove(listener);
if (remaining != null)
{
failureListener = remaining;
return true;
}
}
return false;
}
protected static void notifyFailure(Request.FailureListener listener, Request request, Throwable failure)
@ -280,4 +434,165 @@ public class RequestListeners implements Dumpable
return name + " = " + listener;
}
}
private static class Link<T, L extends Link<T, L>>
{
private final Class<L> type;
private final BiFunction<T, T, L> ctor;
protected final T prev;
protected final T next;
protected Link(Class<L> type, BiFunction<T, T, L> ctor, T prev, T next)
{
this.type = type;
this.ctor = ctor;
this.prev = prev;
this.next = next;
}
@SuppressWarnings("unchecked")
protected T remove(T listener)
{
// The add methods build a fold-left structure:
// f = Link3(Link2(Link1(listener1, listener2), listener3), listener4)
// f.remove(listener1) yields: fa = Link3a(Link2a(listener2, listener3), listener4)
// f.remove(listener4) yields: fa = Link2(Link1(listener1, listener2), listener3)
// First check next, to optimize the case where listeners
// are removed in reverse order (we would not allocate).
// If there is a match on next, return the other component.
if (next == listener)
return prev;
// If it is a link, delegate the removal to it.
if (type.isInstance(prev))
{
T remaining = type.cast(prev).remove(listener);
// The prev link was modified by the removal,
// rebuild this link with the modification.
if (remaining != null)
return (T)ctor.apply(remaining, next);
// Not found.
return null;
}
// If there is a match on prev, return the other component.
if (prev == listener)
return next;
// Not found.
return null;
}
@Override
public String toString()
{
return "%s@%x(%s,%s)".formatted(getClass().getSimpleName(), hashCode(), prev, next);
}
}
private static class QueuedListenerLink extends Link<Request.QueuedListener, QueuedListenerLink> implements Request.QueuedListener
{
private QueuedListenerLink(Request.QueuedListener prev, Request.QueuedListener next)
{
super(QueuedListenerLink.class, QueuedListenerLink::new, prev, next);
}
@Override
public void onQueued(Request request)
{
notifyQueued(prev, request);
notifyQueued(next, request);
}
}
private static class BeginListenerLink extends Link<Request.BeginListener, BeginListenerLink> implements Request.BeginListener
{
private BeginListenerLink(Request.BeginListener prev, Request.BeginListener next)
{
super(BeginListenerLink.class, BeginListenerLink::new, prev, next);
}
@Override
public void onBegin(Request request)
{
notifyBegin(prev, request);
notifyBegin(next, request);
}
}
private static class HeadersListenerLink extends Link<Request.HeadersListener, HeadersListenerLink> implements Request.HeadersListener
{
private HeadersListenerLink(Request.HeadersListener prev, Request.HeadersListener next)
{
super(HeadersListenerLink.class, HeadersListenerLink::new, prev, next);
}
@Override
public void onHeaders(Request request)
{
notifyHeaders(prev, request);
notifyHeaders(next, request);
}
}
private static class CommitListenerLink extends Link<Request.CommitListener, CommitListenerLink> implements Request.CommitListener
{
private CommitListenerLink(Request.CommitListener prev, Request.CommitListener next)
{
super(CommitListenerLink.class, CommitListenerLink::new, prev, next);
}
@Override
public void onCommit(Request request)
{
notifyCommit(prev, request);
notifyCommit(next, request);
}
}
private static class ContentListenerLink extends Link<Request.ContentListener, ContentListenerLink> implements Request.ContentListener
{
private ContentListenerLink(Request.ContentListener prev, Request.ContentListener next)
{
super(ContentListenerLink.class, ContentListenerLink::new, prev, next);
}
@Override
public void onContent(Request request, ByteBuffer content)
{
notifyContent(prev, request, content);
notifyContent(next, request, content);
}
}
private static class SuccessListenerLink extends Link<Request.SuccessListener, SuccessListenerLink> implements Request.SuccessListener
{
private SuccessListenerLink(Request.SuccessListener prev, Request.SuccessListener next)
{
super(SuccessListenerLink.class, SuccessListenerLink::new, prev, next);
}
@Override
public void onSuccess(Request request)
{
notifySuccess(prev, request);
notifySuccess(next, request);
}
}
private static class FailureListenerLink extends Link<Request.FailureListener, FailureListenerLink> implements Request.FailureListener
{
private FailureListenerLink(Request.FailureListener prev, Request.FailureListener next)
{
super(FailureListenerLink.class, FailureListenerLink::new, prev, next);
}
@Override
public void onFailure(Request request, Throwable failure)
{
notifyFailure(prev, request, failure);
notifyFailure(next, request, failure);
}
}
}

View File

@ -16,11 +16,11 @@ package org.eclipse.jetty.client;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
@ -32,6 +32,7 @@ import javax.security.auth.login.LoginException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.security.SecurityUtils;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
@ -205,7 +206,7 @@ public class SPNEGOAuthentication extends AbstractAuthentication
String b64Input = headerInfo.getBase64();
byte[] input = b64Input == null ? new byte[0] : Base64.getDecoder().decode(b64Input);
byte[] output = Subject.doAs(spnegoContext.subject, initGSSContext(spnegoContext, request.getHost(), input));
byte[] output = SecurityUtils.doAs(spnegoContext.subject, initGSSContext(spnegoContext, request.getHost(), input));
String b64Output = output == null ? null : new String(Base64.getEncoder().encode(output));
// The result cannot be used for subsequent requests,
@ -239,7 +240,7 @@ public class SPNEGOAuthentication extends AbstractAuthentication
}
}
private PrivilegedAction<byte[]> initGSSContext(SPNEGOContext spnegoContext, String host, byte[] bytes)
private Callable<byte[]> initGSSContext(SPNEGOContext spnegoContext, String host, byte[] bytes)
{
return () ->
{

View File

@ -273,7 +273,7 @@ public class HttpRequest implements Request
@Override
public Fields getParams()
{
return new Fields(params, true);
return params.asImmutable();
}
@Override

View File

@ -540,11 +540,13 @@ public abstract class HttpSender
}
}
boolean last = chunk.isLast();
chunk.release();
chunk = null;
if (proceed)
{
if (chunk.isLast())
if (last)
{
success = true;
complete = true;
@ -568,7 +570,10 @@ public abstract class HttpSender
public void failed(Throwable x)
{
if (chunk != null)
{
chunk.release();
chunk = null;
}
HttpRequest request = exchange.getRequest();
Content.Source content = request.getBody();

View File

@ -71,14 +71,17 @@ public class ResponseListeners
completeListener = that.completeListener;
}
public void addBeginListener(Response.BeginListener listener)
public boolean addBeginListener(Response.BeginListener listener)
{
if (listener == null)
return false;
Response.BeginListener existing = beginListener;
beginListener = existing == null ? listener : response ->
{
notifyBegin(existing, response);
notifyBegin(listener, response);
};
return true;
}
public void notifyBegin(Response response)
@ -99,8 +102,10 @@ public class ResponseListeners
}
}
public void addHeaderListener(Response.HeaderListener listener)
public boolean addHeaderListener(Response.HeaderListener listener)
{
if (listener == null)
return false;
Response.HeaderListener existing = headerListener;
headerListener = existing == null ? listener : (response, field) ->
{
@ -108,6 +113,7 @@ public class ResponseListeners
boolean r2 = notifyHeader(listener, response, field);
return r1 && r2;
};
return true;
}
public boolean notifyHeader(Response response, HttpField field)
@ -130,14 +136,17 @@ public class ResponseListeners
}
}
public void addHeadersListener(Response.HeadersListener listener)
public boolean addHeadersListener(Response.HeadersListener listener)
{
if (listener == null)
return false;
Response.HeadersListener existing = headersListener;
headersListener = existing == null ? listener : response ->
{
notifyHeaders(existing, response);
notifyHeaders(listener, response);
};
return true;
}
public void notifyHeaders(Response response)
@ -158,8 +167,10 @@ public class ResponseListeners
}
}
public void addContentSourceListener(Response.ContentSourceListener listener)
public boolean addContentSourceListener(Response.ContentSourceListener listener)
{
if (listener == null)
return false;
Response.ContentSourceListener existing = contentSourceListener;
if (existing == null)
{
@ -179,6 +190,7 @@ public class ResponseListeners
contentSourceListener = demultiplexer;
}
}
return true;
}
public boolean hasContentSourceListeners()
@ -234,14 +246,17 @@ public class ResponseListeners
}
}
public void addSuccessListener(Response.SuccessListener listener)
public boolean addSuccessListener(Response.SuccessListener listener)
{
if (listener == null)
return false;
Response.SuccessListener existing = successListener;
successListener = existing == null ? listener : response ->
{
notifySuccess(existing, response);
notifySuccess(listener, response);
};
return true;
}
public void notifySuccess(Response response)
@ -262,14 +277,17 @@ public class ResponseListeners
}
}
public void addFailureListener(Response.FailureListener listener)
public boolean addFailureListener(Response.FailureListener listener)
{
if (listener == null)
return false;
Response.FailureListener existing = failureListener;
failureListener = existing == null ? listener : (response, failure) ->
{
notifyFailure(existing, response, failure);
notifyFailure(listener, response, failure);
};
return true;
}
public void notifyFailure(Response response, Throwable failure)
@ -290,13 +308,15 @@ public class ResponseListeners
}
}
public void addCompleteListener(Response.CompleteListener listener)
public boolean addCompleteListener(Response.CompleteListener listener)
{
addCompleteListener(listener, true);
return addCompleteListener(listener, true);
}
private void addCompleteListener(Response.CompleteListener listener, boolean includeOtherEvents)
private boolean addCompleteListener(Response.CompleteListener listener, boolean includeOtherEvents)
{
if (listener == null)
return false;
if (includeOtherEvents)
{
if (listener instanceof Response.BeginListener l)
@ -318,6 +338,7 @@ public class ResponseListeners
notifyComplete(existing, result);
notifyComplete(listener, result);
};
return true;
}
public void notifyComplete(Result result)
@ -338,26 +359,28 @@ public class ResponseListeners
}
}
public void addListener(Response.Listener listener)
public boolean addListener(Response.Listener listener)
{
addBeginListener(listener);
addHeaderListener(listener);
addHeadersListener(listener);
addContentSourceListener(listener);
addSuccessListener(listener);
addFailureListener(listener);
addCompleteListener(listener, false);
// Use binary OR to avoid short-circuit.
return addBeginListener(listener) |
addHeaderListener(listener) |
addHeadersListener(listener) |
addContentSourceListener(listener) |
addSuccessListener(listener) |
addFailureListener(listener) |
addCompleteListener(listener, false);
}
public void addResponseListeners(ResponseListeners listeners)
public boolean addResponseListeners(ResponseListeners listeners)
{
addBeginListener(listeners.beginListener);
addHeaderListener(listeners.headerListener);
addHeadersListener(listeners.headersListener);
addContentSourceListener(listeners.contentSourceListener);
addSuccessListener(listeners.successListener);
addFailureListener(listeners.failureListener);
addCompleteListener(listeners.completeListener, false);
// Use binary OR to avoid short-circuit.
return addBeginListener(listeners.beginListener) |
addHeaderListener(listeners.headerListener) |
addHeadersListener(listeners.headersListener) |
addContentSourceListener(listeners.contentSourceListener) |
addSuccessListener(listeners.successListener) |
addFailureListener(listeners.failureListener) |
addCompleteListener(listeners.completeListener, false);
}
private void emitEvents(Response response)

View File

@ -13,21 +13,51 @@
package org.eclipse.jetty.client;
import org.junit.jupiter.api.Disabled;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.IntFunction;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import static org.junit.jupiter.api.Assertions.fail;
import static org.eclipse.jetty.client.Authentication.ANY_REALM;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
// TODO
@Disabled
public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
{
@Test
public void testNeedToUpdateThisTest()
{
fail("This test needs to be updated to use Core version of Basic Auth (when available)");
}
/*
private String realm = "TestRealm";
public void startBasic(Scenario scenario, Handler handler) throws Exception
@ -51,23 +81,16 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
private void start(Scenario scenario, Authenticator authenticator, Handler handler) throws Exception
{
server = new Server();
File realmFile = MavenTestingUtils.getTestResourceFile("realm.properties");
LoginService loginService = new HashLoginService(realm, realmFile.getAbsolutePath());
Path realmFile = MavenTestingUtils.getTestResourcePath("realm.properties");
LoginService loginService = new HashLoginService(realm, ResourceFactory.root().newResource(realmFile));
server.addBean(loginService);
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped();
Constraint constraint = new Constraint.Builder().authorization(Constraint.Authorization.ANY_USER).build();
securityHandler.put("/secure", constraint);
Constraint constraint = new Constraint();
constraint.setAuthenticate(true);
constraint.setRoles(new String[]{"**"}); //allow any authenticated user
ConstraintMapping mapping = new ConstraintMapping();
mapping.setPathSpec("/secure");
mapping.setConstraint(constraint);
securityHandler.addConstraintMapping(mapping);
securityHandler.setAuthenticator(authenticator);
securityHandler.setLoginService(loginService);
securityHandler.setHandler(handler);
start(scenario, securityHandler);
}
@ -133,7 +156,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
AuthenticationStore authenticationStore = client.getAuthenticationStore();
AtomicReference<CountDownLatch> requests = new AtomicReference<>(new CountDownLatch(1));
Request.Listener.Adapter requestListener = new Request.Listener.Adapter()
Request.Listener requestListener = new Request.Listener()
{
@Override
public void onSuccess(Request request)
@ -141,7 +164,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
requests.get().countDown();
}
};
client.getRequestListeners().add(requestListener);
client.getRequestListeners().addListener(requestListener);
// Request without Authentication causes a 401
Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()).path("/secure");
@ -149,12 +172,12 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
assertNotNull(response);
assertEquals(401, response.getStatus());
assertTrue(requests.get().await(5, TimeUnit.SECONDS));
client.getRequestListeners().remove(requestListener);
client.getRequestListeners().removeListener(requestListener);
authenticationStore.addAuthentication(authentication);
requests.set(new CountDownLatch(2));
requestListener = new Request.Listener.Adapter()
requestListener = new Request.Listener()
{
@Override
public void onSuccess(Request request)
@ -162,7 +185,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
requests.get().countDown();
}
};
client.getRequestListeners().add(requestListener);
client.getRequestListeners().addListener(requestListener);
// Request with authentication causes a 401 (no previous successful authentication) + 200
request = client.newRequest("localhost", connector.getLocalPort()).scheme(scenario.getScheme()).path("/secure");
@ -170,10 +193,10 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
assertNotNull(response);
assertEquals(200, response.getStatus());
assertTrue(requests.get().await(5, TimeUnit.SECONDS));
client.getRequestListeners().remove(requestListener);
client.getRequestListeners().removeListener(requestListener);
requests.set(new CountDownLatch(1));
requestListener = new Request.Listener.Adapter()
requestListener = new Request.Listener()
{
@Override
public void onSuccess(Request request)
@ -181,7 +204,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
requests.get().countDown();
}
};
client.getRequestListeners().add(requestListener);
client.getRequestListeners().addListener(requestListener);
// Further requests do not trigger 401 because there is a previous successful authentication
// Remove existing header to be sure it's added by the implementation
@ -190,26 +213,31 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
assertNotNull(response);
assertEquals(200, response.getStatus());
assertTrue(requests.get().await(5, TimeUnit.SECONDS));
client.getRequestListeners().remove(requestListener);
client.getRequestListeners().removeListener(requestListener);
}
@ParameterizedTest
@ArgumentsSource(ScenarioProvider.class)
public void testBasicAuthenticationThenRedirect(Scenario scenario) throws Exception
{
startBasic(scenario, new EmptyServerHandler()
startBasic(scenario, new Handler.Abstract()
{
private final AtomicInteger requests = new AtomicInteger();
@Override
protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback)
{
int r = requests.incrementAndGet();
if (r == 1)
{
String path = request.getRequestURI() + "/" + r;
response.sendRedirect(URIUtil.newURI(scenario.getScheme(), request.getServerName(), request.getServerPort(), path, null));
String location = request.getHttpURI().asString() + "/" + r;
org.eclipse.jetty.server.Response.sendRedirect(request, response, callback, location);
}
else
{
callback.succeeded();
}
return true;
}
});
@ -217,7 +245,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
client.getAuthenticationStore().addAuthentication(new BasicAuthentication(uri, realm, "basic", "basic"));
CountDownLatch requests = new CountDownLatch(3);
Request.Listener.Adapter requestListener = new Request.Listener.Adapter()
Request.Listener requestListener = new Request.Listener()
{
@Override
public void onSuccess(Request request)
@ -225,7 +253,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
requests.countDown();
}
};
client.getRequestListeners().add(requestListener);
client.getRequestListeners().addListener(requestListener);
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(scenario.getScheme())
@ -235,20 +263,28 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
assertNotNull(response);
assertEquals(200, response.getStatus());
assertTrue(requests.await(5, TimeUnit.SECONDS));
client.getRequestListeners().remove(requestListener);
client.getRequestListeners().removeListener(requestListener);
}
@ParameterizedTest
@ArgumentsSource(ScenarioProvider.class)
public void testRedirectThenBasicAuthentication(Scenario scenario) throws Exception
{
startBasic(scenario, new EmptyServerHandler()
startBasic(scenario, new Handler.Abstract()
{
@Override
protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback)
{
if (request.getRequestURI().endsWith("/redirect"))
response.sendRedirect(URIUtil.newURI(scenario.getScheme(), request.getServerName(), request.getServerPort(), "/secure", null));
if (org.eclipse.jetty.server.Request.getPathInContext(request).endsWith("/redirect"))
{
String location = HttpURI.build(request.getHttpURI()).path("/secure").asString();
org.eclipse.jetty.server.Response.sendRedirect(request, response, callback, location);
}
else
{
callback.succeeded();
}
return true;
}
});
@ -256,7 +292,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
client.getAuthenticationStore().addAuthentication(new BasicAuthentication(uri, realm, "basic", "basic"));
CountDownLatch requests = new CountDownLatch(3);
Request.Listener.Adapter requestListener = new Request.Listener.Adapter()
Request.Listener requestListener = new Request.Listener()
{
@Override
public void onSuccess(Request request)
@ -264,7 +300,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
requests.countDown();
}
};
client.getRequestListeners().add(requestListener);
client.getRequestListeners().addListener(requestListener);
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(scenario.getScheme())
@ -274,7 +310,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
assertNotNull(response);
assertEquals(200, response.getStatus());
assertTrue(requests.await(5, TimeUnit.SECONDS));
client.getRequestListeners().remove(requestListener);
client.getRequestListeners().removeListener(requestListener);
}
@ParameterizedTest
@ -284,7 +320,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
startBasic(scenario, new EmptyServerHandler());
AtomicReference<CountDownLatch> requests = new AtomicReference<>(new CountDownLatch(2));
Request.Listener.Adapter requestListener = new Request.Listener.Adapter()
Request.Listener requestListener = new Request.Listener()
{
@Override
public void onSuccess(Request request)
@ -292,7 +328,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
requests.get().countDown();
}
};
client.getRequestListeners().add(requestListener);
client.getRequestListeners().addListener(requestListener);
AuthenticationStore authenticationStore = client.getAuthenticationStore();
URI uri = URI.create(scenario.getScheme() + "://localhost:" + connector.getLocalPort());
@ -397,7 +433,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
authenticationStore.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, "basic", "basic"));
AtomicInteger requests = new AtomicInteger();
client.getRequestListeners().add(new Request.Listener.Adapter()
client.getRequestListeners().addListener(new Request.Listener()
{
@Override
public void onSuccess(Request request)
@ -429,14 +465,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
CountDownLatch resultLatch = new CountDownLatch(1);
byte[] data = new byte[]{'h', 'e', 'l', 'l', 'o'};
AsyncRequestContent content = new AsyncRequestContent(ByteBuffer.wrap(data))
{
@Override
public boolean isReproducible()
{
return false;
}
};
AsyncRequestContent content = new AsyncRequestContent(ByteBuffer.wrap(data));
Request request = client.newRequest(uri)
.path("/secure")
.body(content);
@ -455,12 +484,13 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
@ArgumentsSource(ScenarioProvider.class)
public void testRequestFailsAfterResponse(Scenario scenario) throws Exception
{
startBasic(scenario, new EmptyServerHandler()
startBasic(scenario, new Handler.Abstract()
{
@Override
protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback)
{
IO.readBytes(jettyRequest.getInputStream());
Content.Source.consumeAll(request, callback);
return true;
}
});
@ -469,10 +499,10 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
client.getProtocolHandlers().put(new WWWAuthenticationProtocolHandler(client)
{
@Override
public Listener getResponseListener()
public Response.Listener getResponseListener()
{
Response.Listener listener = super.getResponseListener();
return new Listener.Adapter()
return new Response.Listener()
{
@Override
public void onSuccess(Response response)
@ -500,11 +530,17 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
{
switch (index)
{
case 0:
case 0 ->
{
return ByteBuffer.wrap(new byte[]{'h', 'e', 'l', 'l', 'o'});
case 1:
}
case 1 ->
{
return ByteBuffer.wrap(new byte[]{'w', 'o', 'r', 'l', 'd'});
case 2:
}
case 2 ->
{
// Only fail the first exchange of the conversation.
if (fail.compareAndSet(true, false))
{
// Wait for the 401 response to arrive
@ -522,9 +558,8 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
{
return null;
}
default:
throw new IllegalStateException();
}
default -> throw new IllegalStateException();
}
});
CountDownLatch resultLatch = new CountDownLatch(1);
@ -547,21 +582,23 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
public void testInfiniteAuthentication(Scenario scenario) throws Exception
{
String authType = "Authenticate";
start(scenario, new EmptyServerHandler()
start(scenario, new Handler.Abstract()
{
@Override
protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback)
{
// Always reply with a 401 to see if the client
// can handle an infinite authentication loop.
response.setStatus(HttpStatus.UNAUTHORIZED_401);
response.getHeaders().put(HttpHeader.WWW_AUTHENTICATE, authType);
callback.succeeded();
return true;
}
});
AuthenticationStore authenticationStore = client.getAuthenticationStore();
URI uri = URI.create(scenario.getScheme() + "://localhost:" + connector.getLocalPort());
authenticationStore.addAuthentication(new AbstractAuthentication(uri, Authentication.ANY_REALM)
authenticationStore.addAuthentication(new AbstractAuthentication(uri, ANY_REALM)
{
@Override
public String getType()
@ -600,7 +637,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
{
AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client);
HeaderInfo headerInfo = aph.getHeaderInfo("Digest realm=\"thermostat\", qop=\"auth\", nonce=\"1523430383\"").get(0);
Authentication.HeaderInfo headerInfo = aph.getHeaderInfo("Digest realm=\"thermostat\", qop=\"auth\", nonce=\"1523430383\"").get(0);
assertTrue(headerInfo.getType().equalsIgnoreCase("Digest"));
assertEquals("auth", headerInfo.getParameter("qop"));
assertEquals("thermostat", headerInfo.getParameter("realm"));
@ -625,10 +662,12 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
assertEquals("1523430383", headerInfo.getParameter("nonce"));
// test multiple authentications
List<HeaderInfo> headerInfoList = aph.getHeaderInfo("Digest qop=\"auth\", realm=\"thermostat\", nonce=\"1523430383\", " +
"Digest realm=\"thermostat2\", qop=\"auth2\", nonce=\"4522530354\", " +
"Digest qop=\"auth3\", nonce=\"9523570528\", realm=\"thermostat3\", " +
"Digest qop=\"auth4\", nonce=\"3526435321\"");
List<Authentication.HeaderInfo> headerInfoList = aph.getHeaderInfo("""
Digest qop="auth", realm="thermostat", nonce="1523430383",\
Digest realm="thermostat2", qop="auth2", nonce="4522530354",\
Digest qop="auth3", nonce="9523570528", realm="thermostat3",\
Digest qop="auth4", nonce="3526435321"\
""");
assertTrue(headerInfoList.get(0).getType().equalsIgnoreCase("Digest"));
assertEquals("auth", headerInfoList.get(0).getParameter("qop"));
@ -650,7 +689,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
assertNull(headerInfoList.get(3).getParameter("realm"));
assertEquals("3526435321", headerInfoList.get(3).getParameter("nonce"));
List<HeaderInfo> headerInfos = aph.getHeaderInfo("Newauth realm=\"apps\", type=1, title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"");
List<Authentication.HeaderInfo> headerInfos = aph.getHeaderInfo("Newauth realm=\"apps\", type=1, title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"");
assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Newauth"));
assertEquals("apps", headerInfos.get(0).getParameter("realm"));
assertEquals("1", headerInfos.get(0).getParameter("type"));
@ -666,11 +705,11 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
{
AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client);
HeaderInfo headerInfo = aph.getHeaderInfo("Scheme").get(0);
Authentication.HeaderInfo headerInfo = aph.getHeaderInfo("Scheme").get(0);
assertTrue(headerInfo.getType().equalsIgnoreCase("Scheme"));
assertNull(headerInfo.getParameter("realm"));
List<HeaderInfo> headerInfos = aph.getHeaderInfo("Scheme1 , Scheme2 , Scheme3");
List<Authentication.HeaderInfo> headerInfos = aph.getHeaderInfo("Scheme1 , Scheme2 , Scheme3");
assertEquals(3, headerInfos.size());
assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Scheme1"));
assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Scheme2"));
@ -725,7 +764,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
public void testEqualsInParam()
{
AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client);
HeaderInfo headerInfo;
Authentication.HeaderInfo headerInfo;
headerInfo = aph.getHeaderInfo("Digest realm=\"=the=rmo=stat=\", qop=\"=a=u=t=h=\", nonce=\"=1523430383=\"").get(0);
assertTrue(headerInfo.getType().equalsIgnoreCase("Digest"));
@ -734,7 +773,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
assertEquals("=1523430383=", headerInfo.getParameter("nonce"));
// test multiple authentications
List<HeaderInfo> headerInfoList = aph.getHeaderInfo("Digest qop=\"=au=th=\", realm=\"=ther=mostat=\", nonce=\"=152343=0383=\", " +
List<Authentication.HeaderInfo> headerInfoList = aph.getHeaderInfo("Digest qop=\"=au=th=\", realm=\"=ther=mostat=\", nonce=\"=152343=0383=\", " +
"Digest realm=\"=thermostat2\", qop=\"=auth2\", nonce=\"=4522530354\", " +
"Digest qop=\"auth3=\", nonce=\"9523570528=\", realm=\"thermostat3=\", ");
@ -758,63 +797,58 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
public void testSingleChallengeLooksLikeMultipleChallenges()
{
AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client);
List<HeaderInfo> headerInfoList = aph.getHeaderInfo("Digest param=\",f \"");
List<Authentication.HeaderInfo> headerInfoList = aph.getHeaderInfo("Digest param=\",f \"");
assertEquals(1, headerInfoList.size());
headerInfoList = aph.getHeaderInfo("Digest realm=\"thermostat\", qop=\",Digest realm=hello\", nonce=\"1523430383=\"");
assertEquals(1, headerInfoList.size());
HeaderInfo headerInfo = headerInfoList.get(0);
Authentication.HeaderInfo headerInfo = headerInfoList.get(0);
assertTrue(headerInfo.getType().equalsIgnoreCase("Digest"));
assertEquals(",Digest realm=hello", headerInfo.getParameter("qop"));
assertEquals("thermostat", headerInfo.getParameter("realm"));
assertEquals(headerInfo.getParameter("nonce"), "1523430383=");
}
private static class GeneratingRequestContent extends AbstractRequestContent
private static class GeneratingRequestContent implements Request.Content
{
private final IntFunction<ByteBuffer> generator;
private int index;
private GeneratingRequestContent(IntFunction<ByteBuffer> generator)
{
super("application/octet-stream");
this.generator = generator;
}
@Override
public boolean isReproducible()
public boolean rewind()
{
index = 0;
return true;
}
@Override
protected Subscription newSubscription(Consumer consumer, boolean emitInitialContent)
public Content.Chunk read()
{
return new SubscriptionImpl(consumer, emitInitialContent);
ByteBuffer buffer = generator.apply(index++);
boolean last = false;
if (buffer == null)
{
buffer = BufferUtil.EMPTY_BUFFER;
last = true;
}
return Content.Chunk.from(buffer, last);
}
private class SubscriptionImpl extends AbstractSubscription
@Override
public void demand(Runnable demandCallback)
{
private int index;
demandCallback.run();
}
public SubscriptionImpl(Consumer consumer, boolean emitInitialContent)
{
super(consumer, emitInitialContent);
}
@Override
protected boolean produceContent(Producer producer)
{
ByteBuffer buffer = generator.apply(index++);
boolean last = false;
if (buffer == null)
{
buffer = BufferUtil.EMPTY_BUFFER;
last = true;
}
return producer.produce(buffer, last, Callback.NOOP);
}
@Override
public void fail(Throwable failure)
{
}
}
*/
}

View File

@ -385,7 +385,7 @@ public class NetworkTrafficListenerTest
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.body(new FormRequestContent(fields))
.send();
assertEquals(HttpStatus.FOUND_302, response.getStatus());
assertEquals(HttpStatus.MOVED_TEMPORARILY_302, response.getStatus());
assertTrue(clientOutgoingLatch.await(1, TimeUnit.SECONDS));
assertTrue(serverIncomingLatch.await(1, TimeUnit.SECONDS));

View File

@ -0,0 +1,112 @@
//
// ========================================================================
// 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.client;
import java.util.ArrayList;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class RequestListenersTest
{
@Test
public void testAddRemove()
{
RequestListeners listeners = new RequestListeners();
List<String> events = new ArrayList<>();
Request.Listener listener1 = new Request.Listener()
{
@Override
public void onQueued(Request request)
{
events.add("queued1");
}
};
listeners.addListener(listener1);
Request.Listener listener2 = new Request.Listener()
{
@Override
public void onQueued(Request request)
{
events.add("queued2");
}
};
listeners.addListener(listener2);
Request.Listener listener3 = new Request.Listener()
{
@Override
public void onQueued(Request request)
{
events.add("queued3");
}
};
listeners.addListener(listener3);
Request.Listener listener4 = new Request.Listener()
{
@Override
public void onQueued(Request request)
{
events.add("queued4");
}
};
listeners.addListener(listener4);
listeners.getQueuedListener().onQueued(null);
assertEquals(4, events.size());
assertThat(events, Matchers.contains("queued1", "queued2", "queued3", "queued4"));
events.clear();
boolean removed = listeners.removeListener(listener2);
assertTrue(removed);
listeners.getQueuedListener().onQueued(null);
assertEquals(3, events.size());
assertThat(events, Matchers.contains("queued1", "queued3", "queued4"));
events.clear();
removed = listeners.removeListener(null);
assertFalse(removed);
removed = listeners.removeListener(new Request.Listener() {});
assertFalse(removed);
removed = listeners.removeListener(listener3);
assertTrue(removed);
listeners.getQueuedListener().onQueued(null);
assertEquals(2, events.size());
assertThat(events, Matchers.contains("queued1", "queued4"));
events.clear();
removed = listeners.removeListener(listener4);
assertTrue(removed);
listeners.getQueuedListener().onQueued(null);
assertEquals(1, events.size());
assertThat(events, Matchers.contains("queued1"));
events.clear();
removed = listeners.removeListener(listener1);
assertTrue(removed);
assertNull(listeners.getQueuedListener());
}
}

View File

@ -13,23 +13,52 @@
package org.eclipse.jetty.client.util;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
import org.eclipse.jetty.client.AbstractHttpClientServerTest;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.eclipse.jetty.client.Authentication;
import org.eclipse.jetty.client.AuthenticationStore;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.EmptyServerHandler;
import org.eclipse.jetty.client.InputStreamRequestContent;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.client.SPNEGOAuthentication;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.SPNEGOLoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.SPNEGOAuthenticator;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.session.SessionHandler;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
// TODO
@Disabled
public class SPNEGOAuthenticationTest extends AbstractHttpClientServerTest
{
@Test
public void testNeedToUpdateThisTest()
{
fail("This test needs to be updated to use Core version of SPNEGO (when available)");
}
/*
private static final Logger LOG = LoggerFactory.getLogger(SPNEGOAuthenticationTest.class);
static
@ -43,17 +72,17 @@ public class SPNEGOAuthenticationTest extends AbstractHttpClientServerTest
}
}
private Path testDirPath = MavenTestingUtils.getTargetTestingPath(SPNEGOAuthenticationTest.class.getSimpleName());
private String clientName = "spnego_client";
private String clientPassword = "spnego_client_pwd";
private String serviceName = "srvc";
private String serviceHost = "localhost";
private String realm = "jetty.org";
private Path realmPropsPath = MavenTestingUtils.getTestResourcePath("realm.properties");
private Path serviceKeyTabPath = testDirPath.resolve("service.keytab");
private Path clientKeyTabPath = testDirPath.resolve("client.keytab");
private final Path testDirPath = MavenTestingUtils.getTargetTestingPath(SPNEGOAuthenticationTest.class.getSimpleName());
private final String clientName = "spnego_client";
private final String clientPassword = "spnego_client_pwd";
private final String serviceName = "srvc";
private final String serviceHost = "localhost";
private final String realm = "jetty.org";
private final Path realmPropsPath = MavenTestingUtils.getTestResourcePath("realm.properties");
private final Path serviceKeyTabPath = testDirPath.resolve("service.keytab");
private final Path clientKeyTabPath = testDirPath.resolve("client.keytab");
private SimpleKdcServer kdc;
private ConfigurableSpnegoAuthenticator authenticator;
private SPNEGOAuthenticator authenticator;
@BeforeEach
public void prepare() throws Exception
@ -94,26 +123,19 @@ public class SPNEGOAuthenticationTest extends AbstractHttpClientServerTest
private void startSPNEGO(Scenario scenario, Handler handler) throws Exception
{
server = new Server();
server.setSessionIdManager(new DefaultSessionIdManager(server));
HashLoginService authorizationService = new HashLoginService(realm, realmPropsPath.toString());
ConfigurableSpnegoLoginService loginService = new ConfigurableSpnegoLoginService(realm, AuthorizationService.from(authorizationService, ""));
loginService.addBean(authorizationService);
loginService.setKeyTabPath(serviceKeyTabPath);
loginService.setServiceName(serviceName);
loginService.setHostName(serviceHost);
server.addBean(loginService);
HashLoginService hashLoginService = new HashLoginService(realm, ResourceFactory.of(server).newResource(realmPropsPath));
SPNEGOLoginService spnegoLoginService = new SPNEGOLoginService(realm, hashLoginService);
spnegoLoginService.setKeyTabPath(serviceKeyTabPath);
spnegoLoginService.setServiceName(serviceName);
spnegoLoginService.setHostName(serviceHost);
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
Constraint constraint = new Constraint();
constraint.setAuthenticate(true);
constraint.setRoles(new String[]{"**"}); //allow any authenticated user
ConstraintMapping mapping = new ConstraintMapping();
mapping.setPathSpec("/secure");
mapping.setConstraint(constraint);
securityHandler.addConstraintMapping(mapping);
authenticator = new ConfigurableSpnegoAuthenticator();
SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped();
Constraint constraint = new Constraint.Builder().authorization(Constraint.Authorization.ANY_USER).build();
securityHandler.put("/secure", constraint);
authenticator = new SPNEGOAuthenticator();
securityHandler.setAuthenticator(authenticator);
securityHandler.setLoginService(loginService);
securityHandler.setLoginService(spnegoLoginService);
securityHandler.setHandler(handler);
SessionHandler sessionHandler = new SessionHandler();
@ -169,7 +191,7 @@ public class SPNEGOAuthenticationTest extends AbstractHttpClientServerTest
assertNull(authnResult);
AtomicInteger requests = new AtomicInteger();
client.getRequestListeners().add(new Request.Listener.Adapter()
client.getRequestListeners().addListener(new Request.Listener()
{
@Override
public void onSuccess(Request request)
@ -191,12 +213,13 @@ public class SPNEGOAuthenticationTest extends AbstractHttpClientServerTest
@ArgumentsSource(ScenarioProvider.class)
public void testAuthenticationExpiration(Scenario scenario) throws Exception
{
startSPNEGO(scenario, new EmptyServerHandler()
startSPNEGO(scenario, new Handler.Abstract()
{
@Override
protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback) throws Exception
{
IO.readBytes(request.getInputStream());
Content.Source.consumeAll(request, callback);
return true;
}
});
long timeout = 1000;
@ -213,7 +236,7 @@ public class SPNEGOAuthenticationTest extends AbstractHttpClientServerTest
authenticationStore.addAuthentication(authentication);
AtomicInteger requests = new AtomicInteger();
client.getRequestListeners().add(new Request.Listener.Adapter()
client.getRequestListeners().addListener(new Request.Listener()
{
@Override
public void onSuccess(Request request)
@ -256,5 +279,4 @@ public class SPNEGOAuthenticationTest extends AbstractHttpClientServerTest
// Authentication expired, but POSTs are allowed.
assertEquals(1, requests.get());
}
*/
}

View File

@ -33,6 +33,10 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>

View File

@ -16,9 +16,11 @@ module org.eclipse.jetty.ee
requires org.slf4j;
requires transitive org.eclipse.jetty.io;
requires transitive org.eclipse.jetty.security;
// Only required if using JMX.
requires static org.eclipse.jetty.jmx;
exports org.eclipse.jetty.ee;
exports org.eclipse.jetty.ee.security;
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.ee.security;
import java.util.List;
import java.util.Set;
@ -20,7 +20,7 @@ public interface ConstraintAware
{
List<ConstraintMapping> getConstraintMappings();
Set<String> getRoles();
Set<String> getKnownRoles();
/**
* Set Constraint Mappings and roles.
@ -45,7 +45,7 @@ public interface ConstraintAware
*
* @param role the role
*/
void addRole(String role);
void addKnownRole(String role);
/**
* See Servlet Spec 31, sec 13.8.4, pg 145

View File

@ -11,17 +11,17 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.ee.security;
import org.eclipse.jetty.util.security.Constraint;
import java.util.Arrays;
import org.eclipse.jetty.security.Constraint;
public class ConstraintMapping
{
String _method;
String[] _methodOmissions;
String _pathSpec;
Constraint _constraint;
/**
@ -84,4 +84,16 @@ public class ConstraintMapping
{
return _methodOmissions;
}
@Override
public String toString()
{
return "%s@%x{%s,%s %s -> %s}".formatted(
getClass().getSimpleName(),
hashCode(),
_method,
_methodOmissions == null ? null : Arrays.asList(_methodOmissions),
_pathSpec,
_constraint);
}
}

View File

@ -13,6 +13,8 @@
package org.eclipse.jetty.http;
import java.io.Serial;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
@ -266,8 +268,11 @@ public interface HttpURI
}
}
class Immutable implements HttpURI
class Immutable implements HttpURI, Serializable
{
@Serial
private static final long serialVersionUID = 2245620284548399386L;
private final String _scheme;
private final String _user;
private final String _host;

View File

@ -61,6 +61,6 @@ public abstract class AbstractPathSpec implements PathSpec
@Override
public String toString()
{
return String.format("%s@%s{%s}", getClass().getSimpleName(), Integer.toHexString(hashCode()), getDeclaration());
return String.format("%s@%s{%s,%s}", getClass().getSimpleName(), Integer.toHexString(hashCode()), getGroup(), getDeclaration());
}
}

View File

@ -233,6 +233,21 @@ public class HttpParserTest
assertEquals(-1, _headers);
}
@ParameterizedTest
@ValueSource(strings = {"\r\n", "\n"})
public void testLineParse5(String eoln)
{
ByteBuffer buffer = BufferUtil.toBuffer("GET /ctx/testLoginPage;jsessionid=123456789;other HTTP/1.0" + eoln + eoln, StandardCharsets.UTF_8);
HttpParser.RequestHandler handler = new Handler();
HttpParser parser = new HttpParser(handler);
parseAll(parser, buffer);
assertEquals("GET", _methodOrVersion);
assertEquals("/ctx/testLoginPage;jsessionid=123456789;other", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason);
assertEquals(-1, _headers);
}
@ParameterizedTest
@ValueSource(strings = {"\r\n", "\n"})
public void testLongURLParse(String eoln)

View File

@ -24,6 +24,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue;
@ -594,4 +595,25 @@ public class PathMappingsTest
));
}
@Test
public void testMappingsOrder()
{
PathMappings<String> p = new PathMappings<>();
p.put("/foo/*", "foo");
p.put("*.txt", "txt");
p.put("/foo/bar/bob/*", "foobarbob");
p.put("*.thing.txt", "thingtxt");
p.put("/", "default");
p.put("/foo/bar/*", "foobar");
assertThat(p.getMatches("/foo/bar/bob/some.thing.txt").stream().map(MappedResource::getResource).toList(),
contains(
"foobarbob",
"foobar",
"foo",
"thingtxt",
"txt",
"default"
));
}
}

View File

@ -1,14 +1,14 @@
<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/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10</artifactId>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-core</artifactId>
<version>12.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-ee10-openid</artifactId>
<artifactId>jetty-openid</artifactId>
<name>EE10 :: OpenID</name>
<description>Jetty OpenID Connect infrastructure</description>
<description>Jetty OpenID Connect Infrastructure</description>
<properties>
<bundle-symbolic-name>${project.groupId}.openid</bundle-symbolic-name>
@ -45,11 +45,11 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<artifactId>jetty-security</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-servlet</artifactId>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
@ -59,6 +59,11 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-session</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>

View File

@ -27,13 +27,13 @@
</Call>
<Call name="addBean">
<Arg>
<New id="OpenIdConfiguration" class="org.eclipse.jetty.ee10.security.openid.OpenIdConfiguration">
<New id="OpenIdConfiguration" class="org.eclipse.jetty.security.openid.OpenIdConfiguration">
<Arg name="issuer"><Property name="jetty.openid.provider" deprecated="jetty.openid.openIdProvider"/></Arg>
<Arg name="authorizationEndpoint"><Property name="jetty.openid.provider.authorizationEndpoint"/></Arg>
<Arg name="tokenEndpoint"><Property name="jetty.openid.provider.tokenEndpoint"/></Arg>
<Arg name="clientId"><Property name="jetty.openid.clientId"/></Arg>
<Arg name="clientSecret"><Property name="jetty.openid.clientSecret"/></Arg>
<Arg name="authMethod"><Property name="jetty.openid.authMethod" default="client_secret_post"/></Arg>
<Arg name="authenticationMethod"><Property name="jetty.openid.authenticationMethod" deprecated="jetty.openid.authMethod" default="client_secret_post"/></Arg>
<Arg name="httpClient"><Ref refid="HttpClient"/></Arg>
<Set name="authenticateNewUsers">
<Property name="jetty.openid.authenticateNewUsers" default="false"/>

View File

@ -3,23 +3,20 @@
[description]
Adds OpenId Connect authentication to the server.
[environment]
ee10
[depend]
ee10-security
security
client
[lib]
lib/jetty-util-ajax-${jetty.version}.jar
lib/jetty-ee10-openid-${jetty.version}.jar
lib/jetty-openid-${jetty.version}.jar
[files]
basehome:modules/openid/jetty-ee10-openid-baseloginservice.xml|etc/jetty-ee10-openid-baseloginservice.xml
basehome:modules/openid/jetty-openid-baseloginservice.xml|etc/jetty-openid-baseloginservice.xml
[xml]
etc/jetty-ee10-openid-baseloginservice.xml
etc/jetty-ee10-openid.xml
etc/jetty-openid-baseloginservice.xml
etc/jetty-openid.xml
[ini-template]
## The OpenID Identity Provider's issuer ID (the entire URL *before* ".well-known/openid-configuration")
@ -47,7 +44,7 @@ etc/jetty-ee10-openid.xml
# jetty.openid.sslContextFactory.trustAll=false
## What authentication method to use with the Token Endpoint (client_secret_post, client_secret_basic).
# jetty.openid.authMethod=client_secret_post
# jetty.openid.authenticationMethod=client_secret_post
## Whether the user should be logged out after the idToken expires.
# jetty.openid.logoutWhenIdTokenIsExpired=false

View File

@ -2,7 +2,7 @@
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure>
<!-- Optional code to configure the base LoginService used by the OpenIdLoginService
<New id="BaseLoginService" class="org.eclipse.jetty.ee10.security.HashLoginService">
<New id="BaseLoginService" class="org.eclipse.jetty.security.HashLoginService">
<Set name="config"><SystemProperty name="jetty.home" default="."/>/etc/realm.properties</Set>
<Set name="hotReload">true</Set>
</New>

View File

@ -11,18 +11,18 @@
// ========================================================================
//
import org.eclipse.jetty.ee10.security.openid.OpenIdAuthenticatorFactory;
import org.eclipse.jetty.ee10.servlet.security.Authenticator;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.openid.OpenIdAuthenticatorFactory;
module org.eclipse.jetty.security.openid
{
requires org.eclipse.jetty.util.ajax;
requires transitive org.eclipse.jetty.client;
requires org.eclipse.jetty.ee10.servlet;
requires transitive org.eclipse.jetty.security;
requires org.slf4j;
exports org.eclipse.jetty.ee10.security.openid;
exports org.eclipse.jetty.security.openid;
provides Authenticator.Factory with OpenIdAuthenticatorFactory;
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security.openid;
package org.eclipse.jetty.security.openid;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

View File

@ -11,40 +11,43 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.security.openid;
package org.eclipse.jetty.security.openid;
import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.eclipse.jetty.ee10.servlet.ServletContextRequest;
import org.eclipse.jetty.ee10.servlet.ServletContextResponse;
import org.eclipse.jetty.ee10.servlet.security.Authentication;
import org.eclipse.jetty.ee10.servlet.security.LoginService;
import org.eclipse.jetty.ee10.servlet.security.ServerAuthException;
import org.eclipse.jetty.ee10.servlet.security.UserAuthentication;
import org.eclipse.jetty.ee10.servlet.security.UserIdentity;
import org.eclipse.jetty.ee10.servlet.security.authentication.DeferredAuthentication;
import org.eclipse.jetty.ee10.servlet.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.ee10.servlet.security.authentication.SessionAuthentication;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.security.authentication.SessionAuthentication;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.security.Constraint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -121,7 +124,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
@Override
public void setConfiguration(AuthConfiguration authConfig)
public void setConfiguration(Configuration authConfig)
{
if (_openIdConfiguration == null)
{
@ -131,25 +134,25 @@ public class OpenIdAuthenticator extends LoginAuthenticator
this._openIdConfiguration = ((OpenIdLoginService)loginService).getConfiguration();
}
String redirectPath = authConfig.getInitParameter(REDIRECT_PATH);
String redirectPath = authConfig.getParameter(REDIRECT_PATH);
if (redirectPath != null)
setRedirectPath(redirectPath);
String error = authConfig.getInitParameter(ERROR_PAGE);
String error = authConfig.getParameter(ERROR_PAGE);
if (error != null)
setErrorPage(error);
String logout = authConfig.getInitParameter(LOGOUT_REDIRECT_PATH);
String logout = authConfig.getParameter(LOGOUT_REDIRECT_PATH);
if (logout != null)
setLogoutRedirectPath(logout);
super.setConfiguration(new OpenIdAuthConfiguration(_openIdConfiguration, authConfig));
super.setConfiguration(new OpenIdAuthenticatorConfiguration(_openIdConfiguration, authConfig));
}
@Override
public String getAuthMethod()
public String getAuthenticationType()
{
return Constraint.__OPENID_AUTH;
return Authenticator.OPENID_AUTH;
}
@Deprecated
@ -224,20 +227,19 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
@Override
public UserIdentity login(String username, Object credentials, Request request)
public UserIdentity login(String username, Object credentials, Request request, Response response)
{
if (LOG.isDebugEnabled())
LOG.debug("login {} {} {}", username, credentials, request);
UserIdentity user = super.login(username, credentials, request);
UserIdentity user = super.login(username, credentials, request, response);
if (user != null)
{
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
HttpSession session = servletContextRequest.getHttpServletRequest().getSession();
Authentication cached = new SessionAuthentication(getAuthMethod(), user, credentials);
Session session = request.getSession(true);
AuthenticationState cached = new SessionAuthentication(getAuthenticationType(), user, credentials);
synchronized (session)
{
session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
session.setAttribute(SessionAuthentication.AUTHENTICATED_ATTRIBUTE, cached);
session.setAttribute(CLAIMS, ((OpenIdCredentials)credentials).getClaims());
session.setAttribute(RESPONSE, ((OpenIdCredentials)credentials).getResponse());
session.setAttribute(ISSUER, _openIdConfiguration.getIssuer());
@ -247,30 +249,38 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
@Override
public void logout(Request request)
public void logout(Request request, Response response)
{
attemptLogoutRedirect(request);
logoutWithoutRedirect(request);
attemptLogoutRedirect(request, response);
logoutWithoutRedirect(request, response);
}
private void logoutWithoutRedirect(Request request)
private void logoutWithoutRedirect(Request request, Response response)
{
super.logout(request);
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
HttpSession session = servletContextRequest.getHttpServletRequest().getSession(false);
super.logout(request, response);
Session session = request.getSession(false);
if (session == null)
return;
synchronized (session)
{
session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
session.removeAttribute(SessionAuthentication.AUTHENTICATED_ATTRIBUTE);
session.removeAttribute(CLAIMS);
session.removeAttribute(RESPONSE);
session.removeAttribute(ISSUER);
}
}
private boolean hasExpiredIdToken(Session session)
{
if (session != null)
{
Map<String, Object> claims = (Map<String, Object>)session.getAttribute(CLAIMS);
if (claims != null)
return OpenIdCredentials.checkExpiry(claims);
}
return false;
}
/**
* <p>This will attempt to redirect the request to the end_session_endpoint, and finally to the {@link #REDIRECT_PATH}.</p>
*
@ -282,30 +292,27 @@ public class OpenIdAuthenticator extends LoginAuthenticator
*
* @param request the request to redirect.
*/
private void attemptLogoutRedirect(Request request)
private void attemptLogoutRedirect(Request request, Response response)
{
try
{
ServletContextRequest baseRequest = Request.as(request, ServletContextRequest.class);
ServletContextResponse baseResponse = baseRequest.getResponse();
HttpServletRequest httpServletRequest = baseRequest.getHttpServletRequest();
HttpServletResponse httpServletResponse = baseResponse.getHttpServletResponse();
String endSessionEndpoint = _openIdConfiguration.getEndSessionEndpoint();
String redirectUri = null;
if (_logoutRedirectPath != null)
{
StringBuilder sb = new StringBuilder(128);
URIUtil.appendSchemeHostPort(sb, httpServletRequest.getScheme(), httpServletRequest.getServerName(), httpServletRequest.getServerPort());
sb.append(httpServletRequest.getContextPath());
sb.append(_logoutRedirectPath);
redirectUri = sb.toString();
HttpURI.Mutable httpURI = HttpURI.build()
.scheme(request.getHttpURI().getScheme())
.host(Request.getServerName(request))
.port(Request.getServerPort(request))
.path(URIUtil.compactPath(Request.getContextPath(request) + _logoutRedirectPath));
redirectUri = httpURI.toString();
}
HttpSession session = baseRequest.getHttpServletRequest().getSession(false);
Session session = request.getSession(false);
if (endSessionEndpoint == null || session == null)
{
if (redirectUri != null)
httpServletResponse.sendRedirect(redirectUri);
sendRedirect(request, response, redirectUri);
return;
}
@ -313,13 +320,13 @@ public class OpenIdAuthenticator extends LoginAuthenticator
if (!(openIdResponse instanceof Map))
{
if (redirectUri != null)
httpServletResponse.sendRedirect(redirectUri);
sendRedirect(request, response, redirectUri);
return;
}
@SuppressWarnings("rawtypes")
String idToken = (String)((Map)openIdResponse).get("id_token");
httpServletResponse.sendRedirect(endSessionEndpoint +
sendRedirect(request, response, endSessionEndpoint +
"?id_token_hint=" + UrlEncoded.encodeString(idToken, StandardCharsets.UTF_8) +
((redirectUri == null) ? "" : "&post_logout_redirect_uri=" + UrlEncoded.encodeString(redirectUri, StandardCharsets.UTF_8)));
}
@ -329,127 +336,138 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
}
@Override
public void prepareRequest(Request request)
private void sendRedirect(Request request, Response response, String location) throws IOException
{
//if this is a request resulting from a redirect after auth is complete
//(ie its from a redirect to the original request uri) then due to
//browser handling of 302 redirects, the method may not be the same as
//that of the original request. Replace the method and original post
//params (if it was a post).
//
//See Servlet Spec 3.1 sec 13.6.3
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
HttpServletRequest httpRequest = servletContextRequest.getHttpServletRequest();
HttpSession session = httpRequest.getSession(false);
if (session == null)
return; //not authenticated yet
String juri;
String method;
synchronized (session)
try (Blocker.Callback callback = Blocker.callback())
{
if (session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null)
return; //not authenticated yet
juri = (String)session.getAttribute(J_URI);
if (juri == null || juri.length() == 0)
return; //no original uri saved
method = (String)session.getAttribute(J_METHOD);
if (method == null || method.length() == 0)
return; //didn't save original request method
Response.sendRedirect(request, response, callback, location);
callback.block();
}
StringBuffer buf = httpRequest.getRequestURL();
if (httpRequest.getQueryString() != null)
buf.append("?").append(httpRequest.getQueryString());
if (!juri.equals(buf.toString()))
return; //this request is not for the same url as the original
// Restore the original request's method on this request.
if (LOG.isDebugEnabled())
LOG.debug("Restoring original method {} for {} with method {}", method, juri, httpRequest.getMethod());
/*
TODO: Need to wrap the request for this.
Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
baseRequest.setMethod(method);
*/
}
private boolean hasExpiredIdToken(HttpSession session)
{
if (session != null)
{
Map<String, Object> claims = (Map)session.getAttribute(CLAIMS);
if (claims != null)
return OpenIdCredentials.checkExpiry(claims);
}
return false;
}
@Override
public Authentication validateRequest(Request req, Response res, Callback cb, boolean mandatory) throws ServerAuthException
public Request prepareRequest(Request request, AuthenticationState authenticationState)
{
ServletContextRequest servletContextRequest = Request.as(req, ServletContextRequest.class);
final HttpServletRequest request = servletContextRequest.getHttpServletRequest();
final HttpServletResponse response = servletContextRequest.getHttpServletResponse();
final Request baseRequest = req;
final Response baseResponse = res;
// if this is a request resulting from a redirect after auth is complete
// (ie its from a redirect to the original request uri) then due to
// browser handling of 302 redirects, the method may not be the same as
// that of the original request. Replace the method and original post
// params (if it was a post).
if (authenticationState instanceof AuthenticationState.Succeeded)
{
Session session = request.getSession(false);
if (session == null)
return request; //not authenticated yet
HttpURI juri = (HttpURI)session.getAttribute(J_URI);
HttpURI uri = request.getHttpURI();
if ((uri.equals(juri)))
{
session.removeAttribute(J_URI);
Fields fields = (Fields)session.removeAttribute(J_POST);
if (fields != null)
request.setAttribute(FormFields.class.getName(), fields);
String method = (String)session.removeAttribute(J_METHOD);
if (method != null && request.getMethod().equals(method))
{
return new Request.Wrapper(request)
{
@Override
public String getMethod()
{
return method;
}
};
}
}
}
return request;
}
protected Fields getParameters(Request request)
{
try
{
Fields queryFields = Request.extractQueryParameters(request);
Fields formFields = FormFields.from(request).get();
return Fields.combine(queryFields, formFields);
}
catch (InterruptedException | ExecutionException e)
{
throw new RuntimeException(e);
}
}
@Override
public Constraint.Authorization getConstraintAuthentication(String pathInContext, Constraint.Authorization existing, Function<Boolean, Session> getSession)
{
Session session = getSession.apply(false);
if (_openIdConfiguration.isLogoutWhenIdTokenIsExpired() && hasExpiredIdToken(session))
return Constraint.Authorization.ANY_USER;
if (isJSecurityCheck(pathInContext))
return Constraint.Authorization.ANY_USER;
if (isErrorPage(pathInContext))
return Constraint.Authorization.ALLOWED;
return existing;
}
@Override
public AuthenticationState validateRequest(Request request, Response response, Callback cb) throws ServerAuthException
{
if (LOG.isDebugEnabled())
LOG.debug("validateRequest({},{},{})", req, res, mandatory);
LOG.debug("validateRequest({},{})", request, response);
String uri = request.getRequestURI();
String uri = request.getHttpURI().toString();
if (uri == null)
uri = "/";
HttpSession session = request.getSession(false);
Session session = request.getSession(false);
if (_openIdConfiguration.isLogoutWhenIdTokenIsExpired() && hasExpiredIdToken(session))
{
// After logout, fall through to the code below and send another login challenge.
logoutWithoutRedirect(req);
// If we expired a valid authentication we do not want to defer authentication,
// we want to try re-authenticate the user.
mandatory = true;
logoutWithoutRedirect(request, response);
}
mandatory |= isJSecurityCheck(uri);
if (!mandatory)
return new DeferredAuthentication(this);
if (isErrorPage(Request.getPathInContext(baseRequest)) && !DeferredAuthentication.isDeferred(res))
return new DeferredAuthentication(this);
try
{
// Get the Session.
if (session == null)
session = request.getSession(true);
if (session == null)
{
sendError(request, response, cb, "session could not be created");
return AuthenticationState.SEND_FAILURE;
}
// TODO: No session API to work this out?
/*
if (request.isRequestedSessionIdFromURL())
{
sendError(req, res, cb, "Session ID must be a cookie to support OpenID authentication");
return Authentication.SEND_FAILURE;
}
*/
// Handle a request for authentication.
if (isJSecurityCheck(uri))
{
String authCode = request.getParameter("code");
Fields parameters = getParameters(request);
String authCode = parameters.getValue("code");
if (authCode == null)
{
sendError(req, res, cb, "auth failed: no code parameter");
return Authentication.SEND_FAILURE;
sendError(request, response, cb, "auth failed: no code parameter");
return AuthenticationState.SEND_FAILURE;
}
String state = request.getParameter("state");
String state = parameters.getValue("state");
if (state == null)
{
sendError(req, res, cb, "auth failed: no state parameter");
return Authentication.SEND_FAILURE;
sendError(request, response, cb, "auth failed: no state parameter");
return AuthenticationState.SEND_FAILURE;
}
// Verify anti-forgery state token.
@ -460,73 +478,71 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
if (uriRedirectInfo == null)
{
sendError(req, res, cb, "auth failed: invalid state parameter");
return Authentication.SEND_FAILURE;
sendError(request, response, cb, "auth failed: invalid state parameter");
return AuthenticationState.SEND_FAILURE;
}
// Attempt to login with the provided authCode.
OpenIdCredentials credentials = new OpenIdCredentials(authCode, getRedirectUri(request));
UserIdentity user = login(null, credentials, req);
UserIdentity user = login(null, credentials, request, response);
if (user == null)
{
sendError(req, res, cb, null);
return Authentication.SEND_FAILURE;
sendError(request, response, cb, null);
return AuthenticationState.SEND_FAILURE;
}
OpenIdAuthentication openIdAuth = new OpenIdAuthentication(getAuthMethod(), user);
LoginAuthenticator.UserAuthenticationSent openIdAuth = new LoginAuthenticator.UserAuthenticationSent(getAuthenticationType(), user);
if (LOG.isDebugEnabled())
LOG.debug("authenticated {}->{}", openIdAuth, uriRedirectInfo.getUri());
// Save redirect info in session so original request can be restored after redirect.
synchronized (session)
{
session.setAttribute(J_URI, uriRedirectInfo.getUri());
// TODO: We are duplicating this logic.
session.setAttribute(J_URI, uriRedirectInfo.getUri().asImmutable());
session.setAttribute(J_METHOD, uriRedirectInfo.getMethod());
session.setAttribute(J_POST, uriRedirectInfo.getFormParameters());
}
// Redirect to the original URI.
response.setContentLength(0);
int redirectCode = req.getConnectionMetaData().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER;
Response.sendRedirect(req, res, cb, redirectCode, uriRedirectInfo.getUri(), true);
response.getHeaders().putLongField(HttpHeader.CONTENT_LENGTH, 0);
int redirectCode = request.getConnectionMetaData().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
? HttpStatus.MOVED_TEMPORARILY_302 : HttpStatus.SEE_OTHER_303;
Response.sendRedirect(request, response, cb, redirectCode, uriRedirectInfo.getUri().toString(), true);
return openIdAuth;
}
// Look for cached authentication in the Session.
Authentication authentication = (Authentication)session.getAttribute(SessionAuthentication.__J_AUTHENTICATED);
if (authentication != null)
AuthenticationState authenticationState = (AuthenticationState)session.getAttribute(SessionAuthentication.AUTHENTICATED_ATTRIBUTE);
if (authenticationState != null)
{
// Has authentication been revoked?
if (authentication instanceof Authentication.User && _loginService != null &&
!_loginService.validate(((Authentication.User)authentication).getUserIdentity()))
if (authenticationState instanceof AuthenticationState.Succeeded && _loginService != null &&
!_loginService.validate(((AuthenticationState.Succeeded)authenticationState).getUserIdentity()))
{
if (LOG.isDebugEnabled())
LOG.debug("auth revoked {}", authentication);
logoutWithoutRedirect(req);
LOG.debug("auth revoked {}", authenticationState);
logoutWithoutRedirect(request, response);
}
else
{
synchronized (session)
{
String jUri = (String)session.getAttribute(J_URI);
HttpURI jUri = (HttpURI)session.getAttribute(J_URI);
if (jUri != null)
{
// Check if the request is for the same url as the original and restore params if it was a post.
if (LOG.isDebugEnabled())
LOG.debug("auth retry {}->{}", authentication, jUri);
StringBuffer buf = request.getRequestURL();
if (request.getQueryString() != null)
buf.append("?").append(request.getQueryString());
LOG.debug("auth retry {}->{}", authenticationState, jUri);
if (jUri.equals(buf.toString()))
if (jUri.equals(request.getHttpURI()))
{
@SuppressWarnings("unchecked")
MultiMap<String> jPost = (MultiMap<String>)session.getAttribute(J_POST);
if (jPost != null)
{
if (LOG.isDebugEnabled())
LOG.debug("auth rePOST {}->{}", authentication, jUri);
LOG.debug("auth rePOST {}->{}", authenticationState, jUri);
// TODO:
// baseRequest.setContentParameters(jPost);
}
@ -536,29 +552,58 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
}
}
if (LOG.isDebugEnabled())
LOG.debug("auth {}", authentication);
return authentication;
LOG.debug("auth {}", authenticationState);
return authenticationState;
}
}
// If we can't send challenge.
if (DeferredAuthentication.isDeferred(res))
if (AuthenticationState.Deferred.isDeferred(response))
{
if (LOG.isDebugEnabled())
LOG.debug("auth deferred {}", session.getId());
return Authentication.UNAUTHENTICATED;
return null;
}
// Send the the challenge.
String challengeUri = getChallengeUri(baseRequest);
// Save the current URI
synchronized (session)
{
// But only if it is not set already, or we save every uri that leads to a login form redirect
if (session.getAttribute(J_URI) == null || _alwaysSaveUri)
{
HttpURI juri = request.getHttpURI();
session.setAttribute(J_URI, juri.asImmutable());
if (!HttpMethod.GET.is(request.getMethod()))
session.setAttribute(J_METHOD, request.getMethod());
if (HttpMethod.POST.is(request.getMethod()))
{
try
{
session.setAttribute(J_POST, FormFields.from(request).get());
}
catch (ExecutionException e)
{
throw new ServerAuthException(e.getCause());
}
catch (InterruptedException e)
{
throw new ServerAuthException(e);
}
}
}
}
// Send the challenge.
String challengeUri = getChallengeUri(request);
if (LOG.isDebugEnabled())
LOG.debug("challenge {}->{}", session.getId(), challengeUri);
int redirectCode = req.getConnectionMetaData().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER;
Response.sendRedirect(req, res, cb, redirectCode, challengeUri, true);
return Authentication.SEND_CONTINUE;
int redirectCode = request.getConnectionMetaData().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
? HttpStatus.MOVED_TEMPORARILY_302 : HttpStatus.SEE_OTHER_303;
Response.sendRedirect(request, response, cb, redirectCode, challengeUri, true);
return AuthenticationState.CHALLENGE;
}
catch (IOException e)
{
@ -577,10 +622,6 @@ public class OpenIdAuthenticator extends LoginAuthenticator
*/
private void sendError(Request request, Response response, Callback callback, String message) throws IOException
{
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
final HttpServletRequest httpServletRequest = servletContextRequest.getHttpServletRequest();
final HttpServletResponse httpServletResponse = servletContextRequest.getHttpServletResponse();
if (LOG.isDebugEnabled())
LOG.debug("OpenId authentication FAILED: {}", message);
@ -589,22 +630,23 @@ public class OpenIdAuthenticator extends LoginAuthenticator
if (LOG.isDebugEnabled())
LOG.debug("auth failed 403");
if (response != null)
httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("auth failed {}", _errorPage);
String redirectUri = URIUtil.addPaths(httpServletRequest.getContextPath(), _errorPage);
String contextPath = Request.getContextPath(request);
String redirectUri = URIUtil.addPaths(contextPath, _errorPage);
if (message != null)
{
String query = URIUtil.addQueries(ERROR_PARAMETER + "=" + UrlEncoded.encodeString(message), _errorQuery);
redirectUri = URIUtil.addPathQuery(URIUtil.addPaths(httpServletRequest.getContextPath(), _errorPath), query);
redirectUri = URIUtil.addPathQuery(URIUtil.addPaths(contextPath, _errorPath), query);
}
int redirectCode = request.getConnectionMetaData().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()
? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER;
? HttpStatus.MOVED_TEMPORARILY_302 : HttpStatus.SEE_OTHER_303;
Response.sendRedirect(request, response, callback, redirectCode, redirectUri, true);
}
}
@ -636,20 +678,9 @@ public class OpenIdAuthenticator extends LoginAuthenticator
return redirectUri.toString();
}
private String getRedirectUri(HttpServletRequest request)
{
final StringBuffer redirectUri = new StringBuffer(128);
URIUtil.appendSchemeHostPort(redirectUri, request.getScheme(),
request.getServerName(), request.getServerPort());
redirectUri.append(request.getContextPath());
redirectUri.append(_redirectPath);
return redirectUri.toString();
}
protected String getChallengeUri(Request request)
{
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
HttpSession session = servletContextRequest.getServletApiRequest().getSession();
Session session = request.getSession(true);
String antiForgeryToken;
synchronized (session)
{
@ -673,13 +704,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
"&response_type=code";
}
@Override
public boolean secureResponse(Request req, Response res, Callback callback, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException
{
return req.isSecure();
}
private UriRedirectInfo removeAndClearCsrfMap(HttpSession session, String csrf)
private UriRedirectInfo removeAndClearCsrfMap(Session session, String csrf)
{
@SuppressWarnings("unchecked")
Map<String, UriRedirectInfo> csrfMap = (Map<String, UriRedirectInfo>)session.getAttribute(CSRF_MAP);
@ -691,7 +716,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
return uriRedirectInfo;
}
private Map<String, UriRedirectInfo> ensureCsrfMap(HttpSession session)
private Map<String, UriRedirectInfo> ensureCsrfMap(Session session)
{
@SuppressWarnings("unchecked")
Map<String, UriRedirectInfo> csrfMap = (Map<String, UriRedirectInfo>)session.getAttribute(CSRF_MAP);
@ -705,6 +730,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
private static class MRUMap extends LinkedHashMap<String, UriRedirectInfo>
{
@Serial
private static final long serialVersionUID = 5375723072014233L;
private final int _size;
@ -723,23 +749,22 @@ public class OpenIdAuthenticator extends LoginAuthenticator
private static class UriRedirectInfo implements Serializable
{
@Serial
private static final long serialVersionUID = 139567755844461433L;
private final String _uri;
private final HttpURI _uri;
private final String _method;
private final MultiMap<String> _formParameters;
public UriRedirectInfo(Request request)
{
_uri = request.getHttpURI().asString();
_uri = request.getHttpURI();
_method = request.getMethod();
// TODO:
if (MimeTypes.Type.FORM_ENCODED.is(request.getHeaders().get(HttpHeader.CONTENT_TYPE)) && HttpMethod.POST.is(request.getMethod()))
{
MultiMap<String> formParameters = new MultiMap<>();
// request.extractFormParameters(formParameters);
_formParameters = formParameters;
// TODO request.extractFormParameters(formParameters);
_formParameters = new MultiMap<>();
}
else
{
@ -747,7 +772,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
}
}
public String getUri()
public HttpURI getUri()
{
return _uri;
}
@ -762,23 +787,4 @@ public class OpenIdAuthenticator extends LoginAuthenticator
return _formParameters;
}
}
/**
* This Authentication represents a just completed OpenId Connect authentication.
* Subsequent requests from the same user are authenticated by the presents
* of a {@link SessionAuthentication} instance in their session.
*/
public static class OpenIdAuthentication extends UserAuthentication implements Authentication.ResponseSent
{
public OpenIdAuthentication(String method, UserIdentity userIdentity)
{
super(method, userIdentity);
}
@Override
public String toString()
{
return "OpenId" + super.toString();
}
}
}

View File

@ -11,27 +11,26 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.security.openid;
package org.eclipse.jetty.security.openid;
import org.eclipse.jetty.ee10.servlet.security.Authenticator;
import org.eclipse.jetty.ee10.servlet.security.LoginService;
import org.eclipse.jetty.ee10.servlet.security.WrappedAuthConfiguration;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.LoginService;
/**
* <p>This class is used to wrap the {@link Authenticator.AuthConfiguration} given to the {@link OpenIdAuthenticator}.</p>
* <p>This class is used to wrap the {@link Authenticator.Configuration} given to the {@link OpenIdAuthenticator}.</p>
* <p>When {@link #getLoginService()} method is called, this implementation will always return an instance of
* {@link OpenIdLoginService}. This allows you to configure an {@link OpenIdAuthenticator} using a {@code null}
* LoginService or any alternative LoginService implementation which will be wrapped by the OpenIdLoginService</p>
*/
public class OpenIdAuthConfiguration extends WrappedAuthConfiguration
public class OpenIdAuthenticatorConfiguration extends Authenticator.Configuration.Wrapper
{
private final OpenIdLoginService _openIdLoginService;
public OpenIdAuthConfiguration(OpenIdConfiguration openIdConfiguration, Authenticator.AuthConfiguration authConfiguration)
public OpenIdAuthenticatorConfiguration(OpenIdConfiguration openIdConfiguration, Authenticator.Configuration authenticatorConfiguration)
{
super(authConfiguration);
super(authenticatorConfiguration);
LoginService loginService = authConfiguration.getLoginService();
LoginService loginService = authenticatorConfiguration.getLoginService();
if (loginService instanceof OpenIdLoginService)
{
_openIdLoginService = (OpenIdLoginService)loginService;
@ -40,7 +39,7 @@ public class OpenIdAuthConfiguration extends WrappedAuthConfiguration
{
_openIdLoginService = new OpenIdLoginService(openIdConfiguration, loginService);
if (loginService == null)
_openIdLoginService.setIdentityService(authConfiguration.getIdentityService());
_openIdLoginService.setIdentityService(authenticatorConfiguration.getIdentityService());
}
}

View File

@ -11,24 +11,23 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.security.openid;
package org.eclipse.jetty.security.openid;
import java.util.Collection;
import jakarta.servlet.ServletContext;
import org.eclipse.jetty.ee10.servlet.security.Authenticator;
import org.eclipse.jetty.ee10.servlet.security.IdentityService;
import org.eclipse.jetty.ee10.servlet.security.LoginService;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.security.Constraint;
public class OpenIdAuthenticatorFactory implements Authenticator.Factory
{
@Override
public Authenticator getAuthenticator(Server server, ServletContext context, Authenticator.AuthConfiguration configuration, IdentityService identityService, LoginService loginService)
public Authenticator getAuthenticator(Server server, Context context, Authenticator.Configuration configuration)
{
String auth = configuration.getAuthMethod();
if (Constraint.__OPENID_AUTH.equalsIgnoreCase(auth))
LoginService loginService = configuration.getLoginService();
String auth = configuration.getAuthenticationType();
if (Authenticator.OPENID_AUTH.equalsIgnoreCase(auth))
{
// If we have an OpenIdLoginService we can extract the configuration.
if (loginService instanceof OpenIdLoginService)

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security.openid;
package org.eclipse.jetty.security.openid;
import java.util.ArrayList;
import java.util.Collections;
@ -50,7 +50,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
private final String clientId;
private final String clientSecret;
private final List<String> scopes = new ArrayList<>();
private final String authMethod;
private final String authenticationMethod;
private String authEndpoint;
private String tokenEndpoint;
private String endSessionEndpoint;
@ -90,7 +90,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
* @param tokenEndpoint the URL of the OpenID provider's token endpoint if configured.
* @param clientId OAuth 2.0 Client Identifier valid at the Authorization Server.
* @param clientSecret The client secret known only by the Client and the Authorization Server.
* @param authMethod Authentication method to use with the Token Endpoint.
* @param authenticationMethod Authentication method to use with the Token Endpoint.
* @param httpClient The {@link HttpClient} instance to use.
*/
public OpenIdConfiguration(@Name("issuer") String issuer,
@ -98,10 +98,10 @@ public class OpenIdConfiguration extends ContainerLifeCycle
@Name("tokenEndpoint") String tokenEndpoint,
@Name("clientId") String clientId,
@Name("clientSecret") String clientSecret,
@Name("authMethod") String authMethod,
@Name("authenticationMethod") String authenticationMethod,
@Name("httpClient") HttpClient httpClient)
{
this(issuer, authorizationEndpoint, tokenEndpoint, null, clientId, clientSecret, authMethod, httpClient);
this(issuer, authorizationEndpoint, tokenEndpoint, null, clientId, clientSecret, authenticationMethod, httpClient);
}
/**
@ -112,7 +112,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
* @param endSessionEndpoint the URL of the OpdnID provider's end session endpoint if configured.
* @param clientId OAuth 2.0 Client Identifier valid at the Authorization Server.
* @param clientSecret The client secret known only by the Client and the Authorization Server.
* @param authMethod Authentication method to use with the Token Endpoint.
* @param authenticationMethod Authentication method to use with the Token Endpoint.
* @param httpClient The {@link HttpClient} instance to use.
*/
public OpenIdConfiguration(@Name("issuer") String issuer,
@ -121,7 +121,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
@Name("endSessionEndpoint") String endSessionEndpoint,
@Name("clientId") String clientId,
@Name("clientSecret") String clientSecret,
@Name("authMethod") String authMethod,
@Name("authenticationMethod") String authenticationMethod,
@Name("httpClient") HttpClient httpClient)
{
this.issuer = issuer;
@ -131,7 +131,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
this.endSessionEndpoint = endSessionEndpoint;
this.tokenEndpoint = tokenEndpoint;
this.httpClient = httpClient != null ? httpClient : newHttpClient();
this.authMethod = authMethod == null ? "client_secret_post" : authMethod;
this.authenticationMethod = authenticationMethod == null ? "client_secret_post" : authenticationMethod;
if (this.issuer == null)
throw new IllegalArgumentException("Issuer was not configured");
@ -250,9 +250,9 @@ public class OpenIdConfiguration extends ContainerLifeCycle
return endSessionEndpoint;
}
public String getAuthMethod()
public String getAuthenticationMethod()
{
return authMethod;
return authenticationMethod;
}
public void addScopes(String... scopes)
@ -296,7 +296,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
@Override
public String toString()
{
return String.format("%s@%x{iss=%s, clientId=%s, authEndpoint=%s, authMethod=%s, tokenEndpoint=%s, scopes=%s, authNewUsers=%s}",
getClass().getSimpleName(), hashCode(), issuer, clientId, authEndpoint, authMethod, tokenEndpoint, scopes, authenticateNewUsers);
return String.format("%s@%x{iss=%s, clientId=%s, authEndpoint=%s, authenticator=%s, tokenEndpoint=%s, scopes=%s, authNewUsers=%s}",
getClass().getSimpleName(), hashCode(), issuer, clientId, authEndpoint, authenticationMethod, tokenEndpoint, scopes, authenticateNewUsers);
}
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.security.openid;
package org.eclipse.jetty.security.openid;
import java.io.Serializable;
import java.net.URI;
@ -188,7 +188,7 @@ public class OpenIdCredentials implements Serializable
fields.add("grant_type", "authorization_code");
Request request = configuration.getHttpClient().POST(configuration.getTokenEndpoint());
switch (configuration.getAuthMethod())
switch (configuration.getAuthenticationMethod())
{
case "client_secret_basic":
URI uri = URI.create(configuration.getTokenEndpoint());
@ -200,7 +200,7 @@ public class OpenIdCredentials implements Serializable
fields.add("client_secret", configuration.getClientSecret());
break;
default:
throw new IllegalStateException(configuration.getAuthMethod());
throw new IllegalStateException(configuration.getAuthenticationMethod());
}
FormRequestContent formContent = new FormRequestContent(fields);

View File

@ -11,15 +11,17 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security.openid;
package org.eclipse.jetty.security.openid;
import java.util.Objects;
import java.util.function.Function;
import javax.security.auth.Subject;
import jakarta.servlet.ServletRequest;
import org.eclipse.jetty.ee9.nested.UserIdentity;
import org.eclipse.jetty.ee9.security.IdentityService;
import org.eclipse.jetty.ee9.security.LoginService;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -74,10 +76,10 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
}
@Override
public UserIdentity login(String identifier, Object credentials, ServletRequest req)
public UserIdentity login(String identifier, Object credentials, Request request, Function<Boolean, Session> getOrCreateSession)
{
if (LOG.isDebugEnabled())
LOG.debug("login({}, {}, {})", identifier, credentials, req);
LOG.debug("login({}, {}, {})", identifier, credentials, getOrCreateSession);
OpenIdCredentials openIdCredentials = (OpenIdCredentials)credentials;
try
@ -96,19 +98,11 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
subject.getPrivateCredentials().add(credentials);
subject.setReadOnly();
IdentityService identityService = getIdentityService();
if (loginService != null)
{
UserIdentity userIdentity = loginService.login(openIdCredentials.getUserId(), "", req);
if (userIdentity == null)
{
if (isAuthenticateNewUsers())
return identityService.newUserIdentity(subject, userPrincipal, new String[0]);
return null;
}
return new OpenIdUserIdentity(subject, userPrincipal, userIdentity);
}
return loginService.getUserIdentity(
subject,
userPrincipal,
isAuthenticateNewUsers());
return identityService.newUserIdentity(subject, userPrincipal, new String[0]);
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security.openid;
package org.eclipse.jetty.security.openid;
import java.io.Serializable;
import java.security.Principal;

View File

@ -0,0 +1 @@
org.eclipse.jetty.security.openid.OpenIdAuthenticatorFactory

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security.openid;
package org.eclipse.jetty.security.openid;
import java.util.Map;
import java.util.stream.Stream;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.security.openid;
package org.eclipse.jetty.security.openid;
import java.util.Base64;

View File

@ -11,38 +11,41 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.security.openid;
package org.eclipse.jetty.security.openid;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.security.AbstractLoginService;
import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping;
import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
import org.eclipse.jetty.ee10.servlet.security.LoginService;
import org.eclipse.jetty.ee10.servlet.security.RolePrincipal;
import org.eclipse.jetty.ee10.servlet.security.UserIdentity;
import org.eclipse.jetty.ee10.servlet.security.UserPrincipal;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.security.AbstractLoginService;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.RolePrincipal;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.security.UserPrincipal;
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.session.FileSessionDataStoreFactory;
import org.eclipse.jetty.session.SessionHandler;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Password;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@ -72,62 +75,50 @@ public class OpenIdAuthenticationTest
public void setup(LoginService loginService, Consumer<OpenIdConfiguration> configure) throws Exception
{
openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET);
openIdProvider.start();
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
// Add servlets
context.addServlet(LoginPage.class, "/login");
context.addServlet(LogoutPage.class, "/logout");
context.addServlet(HomePage.class, "/*");
context.addServlet(ErrorPage.class, "/error");
// Set up a local OIDC provider and add its configuration to the Server.
openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET);
openIdProvider.start();
// configure security constraints
Constraint constraint = new Constraint();
constraint.setName(Constraint.__OPENID_AUTH);
constraint.setRoles(new String[]{"**"});
constraint.setAuthenticate(true);
Constraint adminConstraint = new Constraint();
adminConstraint.setName(Constraint.__OPENID_AUTH);
adminConstraint.setRoles(new String[]{"admin"});
adminConstraint.setAuthenticate(true);
// constraint mappings
ConstraintMapping profileMapping = new ConstraintMapping();
profileMapping.setConstraint(constraint);
profileMapping.setPathSpec("/profile");
ConstraintMapping loginMapping = new ConstraintMapping();
loginMapping.setConstraint(constraint);
loginMapping.setPathSpec("/login");
ConstraintMapping adminMapping = new ConstraintMapping();
adminMapping.setConstraint(adminConstraint);
adminMapping.setPathSpec("/admin");
// security handler
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
assertThat(securityHandler.getKnownAuthenticatorFactories().size(), greaterThanOrEqualTo(2));
securityHandler.setAuthMethod(Constraint.__OPENID_AUTH);
securityHandler.setRealmName(openIdProvider.getProvider());
securityHandler.setLoginService(loginService);
securityHandler.addConstraintMapping(profileMapping);
securityHandler.addConstraintMapping(loginMapping);
securityHandler.addConstraintMapping(adminMapping);
// Authentication using local OIDC Provider
OpenIdConfiguration openIdConfiguration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);
if (configure != null)
configure.accept(openIdConfiguration);
server.addBean(openIdConfiguration);
securityHandler.setInitParameter(OpenIdAuthenticator.REDIRECT_PATH, "/redirect_path");
securityHandler.setInitParameter(OpenIdAuthenticator.ERROR_PAGE, "/error");
securityHandler.setInitParameter(OpenIdAuthenticator.LOGOUT_REDIRECT_PATH, "/");
context.setSecurityHandler(securityHandler);
// Configure SecurityHandler.
SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped();
assertThat(securityHandler.getKnownAuthenticatorFactories().size(), greaterThanOrEqualTo(2));
securityHandler.setLoginService(loginService);
securityHandler.setAuthenticationType(Authenticator.OPENID_AUTH);
securityHandler.setRealmName(openIdProvider.getProvider());
// Configure Contexts.
ContextHandlerCollection contexts = new ContextHandlerCollection();
securityHandler.setHandler(contexts);
SessionHandler sessionHandler = new SessionHandler();
sessionHandler.setHandler(securityHandler);
ContextHandler contextHandler = new ContextHandler();
contextHandler.setHandler(sessionHandler);
server.setHandler(contextHandler);
// TODO this is a VERY strange usage of ContextHandlerCollection, as normally
// the Session and Security handlers would be inside a single context.
contexts.addHandler(new LoginPage("/login"));
contexts.addHandler(new LogoutPage("/logout"));
contexts.addHandler(new HomePage("/"));
contexts.addHandler(new ErrorPage("/error"));
// Configure security constraints.
securityHandler.put("/login", Constraint.ANY_USER);
securityHandler.put("/profile", Constraint.ANY_USER);
securityHandler.put("/admin", Constraint.from("admin"));
// Configure Jetty to use the local OIDC provider we have previously configured.
securityHandler.setParameter(OpenIdAuthenticator.REDIRECT_PATH, "/redirect_path");
securityHandler.setParameter(OpenIdAuthenticator.ERROR_PAGE, "/error");
securityHandler.setParameter(OpenIdAuthenticator.LOGOUT_REDIRECT_PATH, "/");
File datastoreDir = MavenTestingUtils.getTargetTestingDir("datastore");
IO.delete(datastoreDir);
@ -135,6 +126,7 @@ public class OpenIdAuthenticationTest
fileSessionDataStoreFactory.setStoreDir(datastoreDir);
server.addBean(fileSessionDataStoreFactory);
// Start the server and add the Servers RedirectURI to the Provider.
server.start();
String redirectUri = "http://localhost:" + connector.getLocalPort() + "/redirect_path";
openIdProvider.addRedirectUri(redirectUri);
@ -364,67 +356,141 @@ public class OpenIdAuthenticationTest
assertThat(content, containsString("email: Alice@example.com"));
}
public static class LoginPage extends HttpServlet
public static class LoginPage extends ContextHandler
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
public LoginPage(String contextPath)
{
response.setContentType("text/html");
response.getWriter().println("success");
response.getWriter().println("<br><a href=\"/\">Home</a>");
super(contextPath);
}
}
public static class LogoutPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
request.logout();
}
}
public static class AdminPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
Map<String, Object> userInfo = (Map<String, Object>)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println(userInfo.get("sub") + ": success");
}
}
public static class HomePage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/html");
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null)
response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html");
try (PrintStream output = new PrintStream(Content.Sink.asOutputStream(response)))
{
Map<String, Object> userInfo = (Map<String, Object>)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println("userId: " + userInfo.get("sub") + "<br>");
response.getWriter().println("name: " + userInfo.get("name") + "<br>");
response.getWriter().println("email: " + userInfo.get("email") + "<br>");
response.getWriter().println("<br><a href=\"/logout\">Logout</a>");
output.println("success");
output.println("<br><a href=\"/\">Home</a>");
output.close();
callback.succeeded();
}
else
catch (Throwable t)
{
response.getWriter().println("not authenticated");
response.getWriter().println("<br><a href=\"/login\">Login</a>");
callback.failed(t);
}
return true;
}
}
public static class ErrorPage extends HttpServlet
public static class LogoutPage extends ContextHandler
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
public LogoutPage(String contextPath)
{
response.setContentType("text/html");
response.getWriter().println("not authorized");
response.getWriter().println("<br><a href=\"/\">Home</a>");
super(contextPath);
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
AuthenticationState.logout(request, response);
callback.succeeded();
return true;
}
}
}
public static class AdminPage extends ContextHandler
{
public AdminPage(String contextPath)
{
super(contextPath);
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html");
try (PrintStream output = new PrintStream(Content.Sink.asOutputStream(response)))
{
Map<String, Object> userInfo = (Map<String, Object>)request.getSession(false).getAttribute(OpenIdAuthenticator.CLAIMS);
output.println(userInfo.get("sub") + ": success");
output.close();
callback.succeeded();
}
catch (Throwable t)
{
callback.failed(t);
}
return true;
}
}
public static class HomePage extends ContextHandler
{
public HomePage(String contextPath)
{
super(contextPath);
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html");
try (PrintStream output = new PrintStream(Content.Sink.asOutputStream(response)))
{
Principal userPrincipal = AuthenticationState.getUserPrincipal(request);
if (userPrincipal != null)
{
Map<String, Object> userInfo = (Map<String, Object>)request.getSession(false).getAttribute(OpenIdAuthenticator.CLAIMS);
output.println("userId: " + userInfo.get("sub") + "<br>");
output.println("name: " + userInfo.get("name") + "<br>");
output.println("email: " + userInfo.get("email") + "<br>");
output.println("<br><a href=\"/logout\">Logout</a>");
}
else
{
output.println("not authenticated");
output.println("<br><a href=\"/login\">Login</a>");
}
output.close();
callback.succeeded();
}
catch (Throwable t)
{
callback.failed(t);
}
return true;
}
}
public static class ErrorPage extends ContextHandler
{
public ErrorPage(String contextPath)
{
super(contextPath);
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html");
try (PrintStream output = new PrintStream(Content.Sink.asOutputStream(response)))
{
output.println("not authorized");
output.println("<br><a href=\"/\">Home</a>");
output.close();
callback.succeeded();
}
catch (Throwable t)
{
callback.failed(t);
}
return true;
}
}
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security.openid;
package org.eclipse.jetty.security.openid;
import java.util.HashMap;
import java.util.Map;

View File

@ -11,10 +11,9 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.security.openid;
package org.eclipse.jetty.security.openid;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
@ -26,14 +25,19 @@ 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.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;
@ -90,13 +94,12 @@ public class OpenIdProvider extends ContainerLifeCycle
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);
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);
}
@ -161,93 +164,118 @@ public class OpenIdProvider extends ContainerLifeCycle
redirectUris.add(uri);
}
public class AuthEndpoint extends HttpServlet
public class AuthEndpoint extends ContextHandler
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
public AuthEndpoint(String contextPath)
{
if (!clientId.equals(req.getParameter("client_id")))
super(contextPath);
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
switch (request.getMethod())
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id");
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 = req.getParameter("redirect_uri");
String redirectUri = parameters.getValue("redirect_uri");
if (!redirectUris.contains(redirectUri))
{
LOG.warn("invalid redirectUri {}", redirectUri);
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri");
return;
}
String scopeString = req.getParameter("scope");
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
String scopeString = parameters.getValue("scope");
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
if (!scopes.contains("openid"))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no openid scope");
return;
}
if (!"code".equals(req.getParameter("response_type")))
if (!"code".equals(parameters.getValue("response_type")))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code");
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "response_type must be code");
return;
}
String state = req.getParameter("state");
String state = parameters.getValue("state");
if (state == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "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>");
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(resp, preAuthedUser, redirectUri, state);
redirectUser(request, response, callback, preAuthedUser, redirectUri, state);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
protected void doPost(Request request, Response response, Callback callback) throws Exception
{
String redirectUri = req.getParameter("redirectUri");
Fields parameters = Request.getParameters(request);
String redirectUri = parameters.getValue("redirectUri");
if (!redirectUris.contains(redirectUri))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri");
return;
}
String state = req.getParameter("state");
String state = parameters.getValue("state");
if (state == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no state param");
return;
}
String username = req.getParameter("username");
String username = parameters.getValue("username");
if (username == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username");
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no username");
return;
}
User user = new User(username);
redirectUser(resp, user, redirectUri, state);
redirectUser(request, response, callback, user, redirectUri, state);
}
public void redirectUser(HttpServletResponse response, User user, String redirectUri, String state) throws IOException
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);
@ -255,7 +283,7 @@ public class OpenIdProvider extends ContainerLifeCycle
try
{
redirectUri += "?code=" + authCode + "&state=" + state;
response.sendRedirect(response.encodeRedirectURL(redirectUri));
Response.sendRedirect(request, response, callback, redirectUri);
}
catch (Throwable t)
{
@ -265,33 +293,40 @@ public class OpenIdProvider extends ContainerLifeCycle
}
}
private class TokenEndpoint extends HttpServlet
private class TokenEndpoint extends ContextHandler
{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
public TokenEndpoint(String contextPath)
{
String code = req.getParameter("code");
super(contextPath);
}
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")) ||
@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)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request");
return;
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "bad auth request");
return true;
}
User user = issuedAuthCodes.remove(code);
if (user == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code");
return;
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid auth code");
return true;
}
String accessToken = "ABCDEFG";
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
String response = "{" +
String content = "{" +
"\"access_token\": \"" + accessToken + "\"," +
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
"\"expires_in\": " + accessTokenDuration + "," +
@ -299,47 +334,55 @@ public class OpenIdProvider extends ContainerLifeCycle
"}";
loggedInUsers.increment();
resp.setContentType("text/plain");
resp.getWriter().print(response);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
response.write(true, BufferUtil.toBuffer(content), callback);
return true;
}
}
private class EndSessionEndpoint extends HttpServlet
private class EndSessionEndpoint extends ContextHandler
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
public EndSessionEndpoint(String contextPath)
{
doPost(req, resp);
super(contextPath);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
String idToken = req.getParameter("id_token_hint");
Fields parameters = Request.getParameters(request);
String idToken = parameters.getValue("id_token_hint");
if (idToken == null)
{
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint");
return;
Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "no id_token_hint");
return true;
}
String logoutRedirect = req.getParameter("post_logout_redirect_uri");
String logoutRedirect = parameters.getValue("post_logout_redirect_uri");
if (logoutRedirect == null)
{
resp.setStatus(HttpServletResponse.SC_OK);
resp.getWriter().println("logout success on end_session_endpoint");
return;
response.setStatus(HttpStatus.OK_200);
response.write(true, BufferUtil.toBuffer("logout success on end_session_endpoint"), callback);
return true;
}
loggedInUsers.decrement();
resp.setContentType("text/plain");
resp.sendRedirect(logoutRedirect);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
Response.sendRedirect(request, response, callback, logoutRedirect);
return true;
}
}
private class ConfigServlet extends HttpServlet
private class ConfigServlet extends ContextHandler
{
public ConfigServlet(String contextPath)
{
super(contextPath);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
String discoveryDocument = "{" +
"\"issuer\": \"" + provider + "\"," +
@ -348,7 +391,8 @@ public class OpenIdProvider extends ContainerLifeCycle
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
"}";
resp.getWriter().write(discoveryDocument);
response.write(true, BufferUtil.toBuffer(discoveryDocument), callback);
return true;
}
}

View File

@ -11,15 +11,13 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security.openid;
package org.eclipse.jetty.security.openid;
import org.eclipse.jetty.ee9.security.Authenticator;
import org.eclipse.jetty.ee9.security.ConstraintSecurityHandler;
import org.eclipse.jetty.ee9.security.LoginService;
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.util.security.Constraint;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
@ -28,20 +26,17 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class OpenIdReamNameTest
public class OpenIdRealmNameTest
{
private final Server server = new Server();
public static ServletContextHandler configureOpenIdContext(String realmName)
public static SecurityHandler configureOpenIdContext(String realmName)
{
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped();
assertThat(securityHandler.getKnownAuthenticatorFactories().size(), greaterThanOrEqualTo(2));
securityHandler.setAuthMethod(Constraint.__OPENID_AUTH);
securityHandler.setAuthenticationType(Authenticator.OPENID_AUTH);
securityHandler.setRealmName(realmName);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/" + realmName);
context.setSecurityHandler(securityHandler);
return context;
return securityHandler;
}
@Test
@ -53,7 +48,7 @@ public class OpenIdReamNameTest
server.addBean(config1);
// Configure two webapps to select configs based on realm name.
ServletContextHandler context1 = configureOpenIdContext("This doesn't matter if only 1 OpenIdConfiguration");
SecurityHandler context1 = configureOpenIdContext("This doesn't matter if only 1 OpenIdConfiguration");
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
server.setHandler(contextHandlerCollection);
@ -63,7 +58,7 @@ public class OpenIdReamNameTest
server.start();
// The OpenIdConfiguration from context1 matches to config1.
Authenticator authenticator = context1.getSecurityHandler().getAuthenticator();
Authenticator authenticator = context1.getAuthenticator();
assertThat(authenticator, instanceOf(OpenIdAuthenticator.class));
LoginService loginService = ((OpenIdAuthenticator)authenticator).getLoginService();
assertThat(loginService, instanceOf(OpenIdLoginService.class));
@ -84,7 +79,7 @@ public class OpenIdReamNameTest
server.addBean(config1);
// Configure two webapps to select configs based on realm name.
ServletContextHandler context1 = configureOpenIdContext(null);
SecurityHandler context1 = configureOpenIdContext(null);
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
server.setHandler(contextHandlerCollection);
@ -94,7 +89,7 @@ public class OpenIdReamNameTest
server.start();
// The OpenIdConfiguration from context1 matches to config1.
Authenticator authenticator = context1.getSecurityHandler().getAuthenticator();
Authenticator authenticator = context1.getAuthenticator();
assertThat(authenticator, instanceOf(OpenIdAuthenticator.class));
LoginService loginService = ((OpenIdAuthenticator)authenticator).getLoginService();
assertThat(loginService, instanceOf(OpenIdLoginService.class));
@ -118,8 +113,8 @@ public class OpenIdReamNameTest
server.addBean(config2);
// Configure two webapps to select configs based on realm name.
ServletContextHandler context1 = configureOpenIdContext(config1.getIssuer());
ServletContextHandler context2 = configureOpenIdContext(config2.getIssuer());
SecurityHandler context1 = configureOpenIdContext(config1.getIssuer());
SecurityHandler context2 = configureOpenIdContext(config2.getIssuer());
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
contextHandlerCollection.addHandler(context2);
@ -130,14 +125,14 @@ public class OpenIdReamNameTest
server.start();
// The OpenIdConfiguration from context1 matches to config1.
Authenticator authenticator = context1.getSecurityHandler().getAuthenticator();
Authenticator authenticator = context1.getAuthenticator();
assertThat(authenticator, instanceOf(OpenIdAuthenticator.class));
LoginService loginService = ((OpenIdAuthenticator)authenticator).getLoginService();
assertThat(loginService, instanceOf(OpenIdLoginService.class));
assertThat(((OpenIdLoginService)loginService).getConfiguration(), Matchers.is(config1));
// The OpenIdConfiguration from context2 matches to config2.
authenticator = context2.getSecurityHandler().getAuthenticator();
authenticator = context2.getAuthenticator();
assertThat(authenticator, instanceOf(OpenIdAuthenticator.class));
loginService = ((OpenIdAuthenticator)authenticator).getLoginService();
assertThat(loginService, instanceOf(OpenIdLoginService.class));
@ -161,7 +156,7 @@ public class OpenIdReamNameTest
server.addBean(config2);
// Configure two webapps to select configs based on realm name.
ServletContextHandler context1 = configureOpenIdContext("provider3");
SecurityHandler context1 = configureOpenIdContext("provider3");
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
server.setHandler(contextHandlerCollection);
@ -173,7 +168,7 @@ public class OpenIdReamNameTest
@Test
public void testNoConfiguration() throws Exception
{
ServletContextHandler context1 = configureOpenIdContext(null);
SecurityHandler context1 = configureOpenIdContext(null);
ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
contextHandlerCollection.addHandler(context1);
server.setHandler(contextHandlerCollection);

View File

@ -0,0 +1,65 @@
<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/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-core</artifactId>
<version>12.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-security</artifactId>
<name>Core :: Security</name>
<description>The common Jetty security implementation</description>
<properties>
<bundle-symbolic-name>${project.groupId}.security</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.jetty.security.*</spotbugs.onlyAnalyze>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
@{argLine}
${jetty.surefire.argLine}
--add-reads org.eclipse.jetty.security=org.eclipse.jetty.logging
</argLine>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-session</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http-tools</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -11,17 +11,17 @@
// ========================================================================
//
module org.eclipse.jetty.ee10.jaas
module org.eclipse.jetty.security
{
requires org.slf4j;
requires org.eclipse.jetty.util;
requires transitive org.eclipse.jetty.ee10.servlet;
// Only required if using JDBCLoginModule.
requires transitive org.eclipse.jetty.server;
requires transitive org.eclipse.jetty.util;
requires transitive org.slf4j;
requires static java.security.jgss;
requires static java.sql;
exports org.eclipse.jetty.ee10.jaas;
exports org.eclipse.jetty.ee10.jaas.callback;
exports org.eclipse.jetty.ee10.jaas.spi;
exports org.eclipse.jetty.security;
exports org.eclipse.jetty.security.authentication;
exports org.eclipse.jetty.security.jaas;
uses org.eclipse.jetty.security.Authenticator.Factory;
}

View File

@ -11,16 +11,16 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javax.security.auth.Subject;
import jakarta.servlet.ServletRequest;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* AbstractLoginService
@ -30,8 +30,6 @@ import org.slf4j.LoggerFactory;
*/
public abstract class AbstractLoginService extends ContainerLifeCycle implements LoginService
{
private static final Logger LOG = LoggerFactory.getLogger(AbstractLoginService.class);
protected IdentityService _identityService = new DefaultIdentityService();
protected String _name;
protected boolean _fullValidate = false;
@ -84,7 +82,7 @@ public abstract class AbstractLoginService extends ContainerLifeCycle implements
}
@Override
public UserIdentity login(String username, Object credentials, ServletRequest request)
public UserIdentity login(String username, Object credentials, Request request, Function<Boolean, Session> getOrCreateSession)
{
if (username == null)
return null;

View File

@ -0,0 +1,348 @@
//
// ========================================================================
// 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;
import java.security.Principal;
import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.IdentityService.RunAsToken;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.security.internal.DeferredAuthenticationState;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
/**
* The Authentication state of a request.
* <p>
* The Authentication state can be one of several sub-types that
* reflects where the request is in the many different authentication
* cycles. Authentication might not yet be checked or it might be checked
* and failed, checked and deferred or succeeded.
*/
public interface AuthenticationState
{
/**
* Get the authentication state of a request
* @param request The request to query
* @return The authentication state of the request or null if none.
*/
static AuthenticationState getAuthenticationState(Request request)
{
Object auth = request.getAttribute(AuthenticationState.class.getName());
return auth instanceof AuthenticationState authenticationState ? authenticationState : null;
}
/**
* Set the authentication state of a request.
* @param request The request to update
* @param authenticationState the state to set on the request.
*/
static void setAuthenticationState(Request request, AuthenticationState authenticationState)
{
request.setAttribute(AuthenticationState.class.getName(), authenticationState);
}
/**
* Get the {@link UserPrincipal} of an authenticated request. If the {@link AuthenticationState}
* is {@link Deferred}, then an attempt to validate is made and the {@link AuthenticationState}
* of the request is updated.
* @see #authenticate(Request)
* @param request The request to query
* @return The {@link UserPrincipal} of any {@link Succeeded} authentication state, potential
* after validating a {@link Deferred} state.
*/
static Principal getUserPrincipal(Request request)
{
Succeeded succeeded = authenticate(request);
if (succeeded == null)
return null;
return succeeded.getUserIdentity().getUserPrincipal();
}
/**
* Get successful authentication for a request. If the {@link AuthenticationState}
* is {@link Deferred}, then an attempt to authenticate, but without sending a challenge.
* @see Deferred#authenticate(Request)
* @see #authenticate(Request, Response, Callback) if an authentication challenge should be sent.
* @param request The request to query.
* @return A {@link Succeeded} authentiction or null
*/
static Succeeded authenticate(Request request)
{
AuthenticationState authenticationState = getAuthenticationState(request);
// resolve any Deferred authentication
if (authenticationState instanceof Deferred deferred)
authenticationState = deferred.authenticate(request);
//if authenticated, return the state
if (authenticationState instanceof Succeeded succeeded)
return succeeded;
// else null
return null;
}
/**
* Get successful authentication for a request. If the {@link AuthenticationState}
* is {@link Deferred}, then an attempt to authenticate, possibly sending a challenge.
* If authentication is not successful, then a {@link HttpStatus#FORBIDDEN_403} response is sent.
* @see Deferred#authenticate(Request, Response, Callback)
* @see #authenticate(Request) if an authentication challenge should not be sent.
* @param request The request to query.
* @param response The response to use for a challenge or error
* @param callback The collback to complete if a challenge or error is sent.
* @return A {@link Succeeded} authentication or null. If null is returned, then the callback
* will be completed.
*/
static Succeeded authenticate(Request request, Response response, Callback callback)
{
AuthenticationState authenticationState = getAuthenticationState(request);
// resolve any Deferred authentication
if (authenticationState instanceof Deferred deferred)
{
authenticationState = deferred.authenticate(request, response, callback);
if (authenticationState instanceof AuthenticationState.ResponseSent)
return null;
}
// if already authenticated, return the state
if (authenticationState instanceof Succeeded succeeded)
return succeeded;
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403);
return null;
}
/**
* Attempt to login a request using the passed credentials.
* The current {@link AuthenticationState} must be {@link Deferred}.
* @see Deferred#login(String, String, Request, Response)
* @param request The request to query.
* @return A {@link Succeeded} authentiction or null
*/
static Succeeded login(String username, String password, Request request, Response response)
{
AuthenticationState authenticationState = getAuthenticationState(request);
// if already authenticated, throw
if (authenticationState instanceof Succeeded)
throw new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Already authenticated");
// Use Deferred authentication to login
if (authenticationState instanceof Deferred deferred)
{
Succeeded undeferred = deferred.login(username, password, request, response);
if (undeferred != null)
{
setAuthenticationState(request, undeferred);
return undeferred;
}
}
return null;
}
static boolean logout(Request request, Response response)
{
AuthenticationState authenticationState = getAuthenticationState(request);
//if already authenticated, return true
if (authenticationState instanceof Succeeded succeededAuthentication)
{
succeededAuthentication.logout(request, response);
return true;
}
if (authenticationState instanceof Deferred deferred)
{
deferred.logout(request, response);
return true;
}
return false;
}
/**
* A successful Authentication with User information.
*/
interface Succeeded extends AuthenticationState
{
/**
* @return The method used to authenticate the user.
*/
String getAuthenticationType();
/**
* @return The {@link UserIdentity} of the authenticated user.
*/
UserIdentity getUserIdentity();
/**
* @param role The role to check.
* @return True if the user is in the passed role
*/
boolean isUserInRole(String role);
/**
* Remove any user information that may be present in the request
* such that a call to getUserPrincipal/getRemoteUser will return null.
*
* @param request the request
* @param response the response
*/
void logout(Request request, Response response);
}
/**
* Authentication Response sent state.
* Responses are sent by authenticators either to issue an
* authentication challenge or on successful authentication in
* order to redirect the user to the original URL.
*/
interface ResponseSent extends AuthenticationState
{
}
/**
* Authentication challenge sent.
* <p>
* This convenience instance is for when an authentication challenge has been sent.
*/
AuthenticationState CHALLENGE = new ResponseSent()
{
@Override
public String toString()
{
return "CHALLENGE";
}
};
/**
* Authentication failure sent.
* <p>
* This convenience instance is for when an authentication failure has been sent.
*/
AuthenticationState SEND_FAILURE = new ResponseSent()
{
@Override
public String toString()
{
return "FAILURE";
}
};
/**
* Authentication success sent.
* <p>
* This convenience instance is for when an authentication success has been sent.
*/
AuthenticationState SEND_SUCCESS = new ResponseSent()
{
@Override
public String toString()
{
return "SEND_SUCCESS";
}
};
static Deferred defer(LoginAuthenticator loginAuthenticator)
{
return new DeferredAuthenticationState(loginAuthenticator);
}
/**
* Authentication is Deferred, either so that credentials can later be passed
* with {@link #login(String, String, Request, Response)}; or that existing
* credentials on the request may be validated with {@link #authenticate(Request)};
* or an authentication dialog can be advanced with {@link #authenticate(Request, Response, Callback)}.
*/
interface Deferred extends AuthenticationState
{
/**
* @param response the response
* @return true if this response is from a deferred call to {@link #authenticate(Request)}
*/
static boolean isDeferred(Response response)
{
return response instanceof DeferredResponse;
}
/**
* Authenticate the request using any credentials already associated with the request.
* No challenge can be sent. If the login is successful, then the
* {@link IdentityService#associate(UserIdentity, RunAsToken)} method is used and the returned
* {@link org.eclipse.jetty.security.IdentityService.Association} is made available via
* {@link #getAssociation()}.
* @see #getAssociation()
* @param request The request to authenticate
* @return A {@link Succeeded} authentication or null
*/
Succeeded authenticate(Request request);
/**
* Authenticate the request using any credentials already associated with the request or
* challenging if necessary. If the login is successful, then the
* {@link IdentityService#associate(UserIdentity, RunAsToken)} method is used and the returned
* {@link org.eclipse.jetty.security.IdentityService.Association} is made available via
* {@link #getAssociation()}.
* @see #getAssociation()
* @param request The request to authenticate
* @param response The response to use for a challenge.
* @param callback The callback to complete if a challenge is sent
* @return The next {@link AuthenticationState}, if it is {@link ResponseSent}, then the
* callback will be completed.
*/
AuthenticationState authenticate(Request request, Response response, Callback callback);
/**
* Authenticate the request with the passed credentials
* @param username The username to validate
* @param password The credential to validate
* @param request The request to authenticate
* @param response The response, which may be updated if the session ID is changed.
* @return A {@link Succeeded} authentication or null
*/
Succeeded login(String username, Object password, Request request, Response response);
/**
* Logout the authenticated user.
* @param request The authenticated request
* @param response The associated response, which may be updated to clear a session ID.
*/
void logout(Request request, Response response);
/**
* @return Any {@link org.eclipse.jetty.security.IdentityService.Association} created during
* deferred authentication.
* @see #authenticate(Request, Response, Callback)
* @see #authenticate(Request)
*/
IdentityService.Association getAssociation();
/**
* A tag interface used to identify a {@link Response} that might be passed to
* {@link Authenticator#validateRequest(Request, Response, Callback)} while
* doing deferred authentication when a challenge cannot be sent.
* @see #authenticate(Request)
*/
interface DeferredResponse extends Response
{
}
}
}

View File

@ -0,0 +1,212 @@
//
// ========================================================================
// 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;
import java.util.Set;
import java.util.function.Function;
import org.eclipse.jetty.security.AuthenticationState.Succeeded;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.Callback;
/**
* Authenticator Interface
* <p>
* An Authenticator is responsible for checking requests and sending
* response challenges in order to authenticate a request.
* Various types of {@link AuthenticationState} are returned in order to
* signal the next step in authentication.
*/
public interface Authenticator
{
String BASIC_AUTH = "BASIC";
String FORM_AUTH = "FORM";
String DIGEST_AUTH = "DIGEST";
String CERT_AUTH = "CLIENT_CERT";
String CERT_AUTH2 = "CLIENT-CERT";
String SPNEGO_AUTH = "SPNEGO";
String NEGOTIATE_AUTH = "NEGOTIATE";
String OPENID_AUTH = "OPENID";
/**
* Configure the Authenticator
*
* @param configuration the configuration
*/
void setConfiguration(Configuration configuration);
/**
* @return The name of the authentication type
*/
String getAuthenticationType();
/**
* Called after {@link #validateRequest(Request, Response, Callback)} and
* before calling {@link org.eclipse.jetty.server.Handler#handle(Request, Response, Callback)}
* of the nested handler.
* This may be used by an {@link Authenticator} to restore method or content from a previous
* request that was challenged.
*
* @param request the request to prepare for handling
* @param authenticationState The authentication for the request
*/
default Request prepareRequest(Request request, AuthenticationState authenticationState)
{
return request;
}
/**
* Get an {@link Constraint.Authorization} applicable to the path for
* this authenticator. This is typically used to vary protection on special URIs known to a
* specific {@link Authenticator} (e.g. /j_security_check for
* the {@link org.eclipse.jetty.security.authentication.FormAuthenticator}.
*
* @param pathInContext The pathInContext to potentially constrain.
* @param existing The existing authentication constraint for the pathInContext determined independently of {@link Authenticator}
* @param getSession Function to get or create a {@link Session}.
* @return The {@link Constraint.Authorization} to apply.
*/
default Constraint.Authorization getConstraintAuthentication(String pathInContext, Constraint.Authorization existing, Function<Boolean, Session> getSession)
{
return existing == null ? Constraint.Authorization.ALLOWED : existing;
}
/**
* Validate a request
*
* @param request The request
* @param response The response
* @param callback the callback to use for writing a response
* @return An Authentication. If Authentication is successful, this will be a {@link Succeeded}. If a response has
* been sent by the Authenticator (which can be done for both successful and unsuccessful authentications), then the result will
* implement {@link AuthenticationState.ResponseSent}.
* @throws ServerAuthException if unable to validate request
*/
AuthenticationState validateRequest(Request request, Response response, Callback callback) throws ServerAuthException;
/**
* Authenticator Configuration
*/
interface Configuration
{
String getAuthenticationType();
String getRealmName();
/**
* Get a SecurityHandler init parameter
*
* @param param parameter name
* @return Parameter value or null
*/
String getParameter(String param);
/**
* Get a SecurityHandler init parameter names
*
* @return Set of parameter names
*/
Set<String> getParameterNames();
LoginService getLoginService();
IdentityService getIdentityService();
boolean isSessionRenewedOnAuthentication();
class Wrapper implements Configuration
{
private final Configuration _configuration;
public Wrapper(Configuration configuration)
{
_configuration = configuration;
}
@Override
public String getAuthenticationType()
{
return _configuration.getAuthenticationType();
}
@Override
public String getRealmName()
{
return _configuration.getRealmName();
}
@Override
public String getParameter(String param)
{
return _configuration.getParameter(param);
}
@Override
public Set<String> getParameterNames()
{
return _configuration.getParameterNames();
}
@Override
public LoginService getLoginService()
{
return _configuration.getLoginService();
}
@Override
public IdentityService getIdentityService()
{
return _configuration.getIdentityService();
}
@Override
public boolean isSessionRenewedOnAuthentication()
{
return _configuration.isSessionRenewedOnAuthentication();
}
}
}
/**
* Authenticator Factory
*/
interface Factory
{
Authenticator getAuthenticator(Server server, Context context, Configuration configuration);
}
class NoOp implements Authenticator
{
@Override
public void setConfiguration(Configuration configuration)
{
}
@Override
public String getAuthenticationType()
{
return null;
}
@Override
public AuthenticationState validateRequest(Request request, Response response, Callback callback) throws ServerAuthException
{
return null;
}
}
}

View File

@ -0,0 +1,364 @@
//
// ========================================================================
// 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;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* A Security constraint that is applied to a request, which contain:
* <ul>
* <li>A name</li>
* <li>Authorization to specify if authentication is needed and what roles are applicable</li>
* <li>An optional list of role names used for {@link Authorization#KNOWN_ROLE}</li>
* <li>A Transport constraint, indicating if it must be secure or not.</li>
* </ul>
* <p>
* The core constraint is not the same as the servlet specification {@code AuthConstraint}, but it is
* sufficiently capable to represent servlet constraints.
* </p>
*/
public interface Constraint
{
/**
* The Authorization applied to any authentication of the request/
*/
enum Authorization
{
/**
* Access not allowed. Equivalent to Servlet AuthConstraint with no roles.
*/
FORBIDDEN,
/**
* Access allowed. Equivalent to Servlet AuthConstraint without any Authorization.
*/
ALLOWED,
/**
* Access allowed for any authenticated user regardless of role. Equivalent to Servlet role "**".
* For example, a web application that defines only an "admin" can use this {@code Authorization} to
* allow any authenticated user known to the configured {@link LoginService}, even if their roles are
* not know to the web application.
*/
ANY_USER,
/**
* Access allowed for authenticated user with any known role. Equivalent to Servlet role "*".
* For example, a web application that defines roles "admin" and "user" might be deployed to a server
* with a configured {@link LoginService} that also has users with "operator" role. This constraint would
* not allow an "operator" user, as that role is not known to the web application.
*/
KNOWN_ROLE,
/**
* Access allowed only for authenticated user with specific role(s).
*/
SPECIFIC_ROLE,
/**
* Inherit the authorization from a less specific constraint when passed to {@link #combine(Constraint, Constraint)},
* otherwise act as {@link #ALLOWED}.
*/
INHERIT;
}
/**
* The constraints requirement for the transport
*/
enum Transport
{
/**
* The transport must be secure (e.g. TLS)
*/
SECURE,
/**
* The transport can be either secure or not secure.
*/
ANY,
/**
* Inherit the transport constraint from a less specific constraint when passed to {@link #combine(Constraint, Constraint)},
* otherwise act as {@link #ANY}.
*/
INHERIT
}
/**
* @return The name for the {@code Constraint} or "unnamed@hashcode" if not named
*/
String getName();
/**
* @return The required {@link Transport} or null if the transport can be either.
*/
Transport getTransport();
/**
* @return The {@link Authorization} criteria applied by this {@code Constraint}
* or null if this constraint does not have any authorization requirements.
*/
Authorization getAuthorization();
/**
* @return The set of roles applied by this {@code Constraint} or the empty set.
*/
Set<String> getRoles();
/**
* Builder for Constraint.
*/
class Builder
{
private String _name;
private Authorization _authorization;
private Set<String> _roles;
private Transport _transport;
public Builder()
{}
public Builder(Constraint constraint)
{
_transport = constraint.getTransport();
_authorization = constraint.getAuthorization();
_roles = constraint.getRoles();
}
public Builder name(String name)
{
_name = name;
return this;
}
public String getName()
{
return _name;
}
public Builder transport(Transport transport)
{
_transport = transport;
return this;
}
public Transport getTransport()
{
return _transport;
}
public Builder authorization(Authorization authorization)
{
_authorization = authorization;
return this;
}
public Authorization getAuthorization()
{
return _authorization;
}
public Builder roles(String... roles)
{
if (roles != null && roles.length > 0)
{
if (_roles == null)
_roles = new HashSet<>();
else if (!(_roles instanceof HashSet<String>))
_roles = new HashSet<>(_roles);
_roles.addAll(Arrays.asList(roles));
}
return this;
}
public Set<String> getRoles()
{
return _roles == null ? Collections.emptySet() : _roles;
}
public Constraint build()
{
return from(_name, _transport, _authorization, _roles);
}
}
/**
* A static Constraint that has {@link Authorization#ALLOWED} and {@link Transport#INHERIT}.
*/
Constraint ALLOWED = from("ALLOWED", Authorization.ALLOWED);
/**
* A static Constraint that has {@link Authorization#FORBIDDEN} and {@link Transport#INHERIT}.
*/
Constraint FORBIDDEN = from("FORBIDDEN", Authorization.FORBIDDEN);
/**
* A static Constraint that has {@link Authorization#ANY_USER} and {@link Transport#INHERIT}.
*/
Constraint ANY_USER = from("ANY_USER", Authorization.ANY_USER);
/**
* A static Constraint that has {@link Authorization#KNOWN_ROLE} and {@link Transport#INHERIT}.
*/
Constraint KNOWN_ROLE = from("KNOWN_ROLE", Authorization.KNOWN_ROLE);
/**
* A static Constraint that has {@link Transport#SECURE} and {@link Authorization#INHERIT}
*/
Constraint SECURE_TRANSPORT = from("SECURE", Transport.SECURE);
/**
* A static Constraint that has {@link Transport#ANY} and {@link Authorization#INHERIT}
*/
Constraint ANY_TRANSPORT = from("ANY", Transport.ANY);
/**
* A static Constraint that has {@link Authorization#ALLOWED} and {@link Transport#ANY}.
*/
Constraint ALLOWED_ANY_TRANSPORT = combine("ALLOWED_ANY_TRANSPORT", ALLOWED, ANY_TRANSPORT);
/**
* Combine two Constraints by using {@link #combine(String, Constraint, Constraint)} with
* a generated name.
* @see #combine(String, Constraint, Constraint)
* @param leastSpecific Constraint to combine
* @param mostSpecific Constraint to combine
* @return the combined constraint.
*/
static Constraint combine(Constraint leastSpecific, Constraint mostSpecific)
{
return combine(null, leastSpecific, mostSpecific);
}
/**
* <p>Combine two Constraints by:</p>
* <ul>
* <li>if both constraints are {@code Null}, then {@link Constraint#ALLOWED} is returned.</li>
* <li>if either constraint is {@code Null} the other is returned.</li>
* <li>only if the {@code mostSpecific} constraint has {@link Authorization#INHERIT} is the
* {@code leastSpecific} constraint's {@link Authorization} used,
* otherwise the {@code mostSpecific}'s is used.</li>
* <li>if the combined constraint has {@link Authorization#SPECIFIC_ROLE}, then the role set from
* the constraint that specified the {@link Authorization#SPECIFIC_ROLE} is used.</li>
* <li>only if the {@code mostSpecific} constraint has {@link Transport#INHERIT} is the
* {@code leastSpecific} constraint's {@link Transport} used,
* otherwise the {@code mostSpecific}'s is used.</li>
* </ul>
* <p>
* Typically the path of the constraint is used to determine which constraint is most specific. For example
* if the following paths mapped to Constraints as:
* </p>
* <pre>
* /* -> Authorization.FORBIDDEN,roles=[],Transport.SECURE
* /admin/* -> Authorization.SPECIFIC_ROLE,roles=["admin"],Transport.INHERIT
* </pre>
* <p>
* The the {@code /admin/*} constraint would be consider most specific and a request to {@code /admin/file} would
* have {@link Authorization#SPECIFIC_ROLE} from the {@code /admin/*} constraint and
* {@link Transport#SECURE} inherited from the {@code /*} constraint. For more examples see
* {@link SecurityHandler.PathMapped}.
* </p>
* <p>Note that this combination is not equivalent to the combination done by the EE servlet specification.</p>
* @param name The name to use for the combined constraint
* @param leastSpecific Constraint to combine
* @param mostSpecific Constraint to combine
* @return the combined constraint.
*/
static Constraint combine(String name, Constraint leastSpecific, Constraint mostSpecific)
{
if (leastSpecific == null)
return mostSpecific == null ? ALLOWED : mostSpecific;
if (mostSpecific == null)
return leastSpecific;
return from(
name,
mostSpecific.getTransport() == Transport.INHERIT ? leastSpecific.getTransport() : mostSpecific.getTransport(),
mostSpecific.getAuthorization() == Authorization.INHERIT ? leastSpecific.getAuthorization() : mostSpecific.getAuthorization(),
mostSpecific.getAuthorization() == Authorization.INHERIT ? leastSpecific.getRoles() : mostSpecific.getRoles());
}
static Constraint from(String... roles)
{
return from(null, Authorization.SPECIFIC_ROLE, roles);
}
static Constraint from(String name, Transport transport)
{
return from(name, transport, null, null);
}
static Constraint from(String name, Authorization authorization, String... roles)
{
return from(name, Transport.INHERIT, authorization, (roles == null || roles.length == 0)
? Collections.emptySet()
: new HashSet<>(Arrays.stream(roles).toList()));
}
static Constraint from(Transport transport, Authorization authorization, Set<String> roles)
{
return from(null, transport, authorization, roles);
}
static Constraint from(String name, Transport transport, Authorization authorization, Set<String> roles)
{
return new Constraint()
{
private final String _name = name == null ? "unnamed@%x".formatted(hashCode()) : name;
private final Transport _transport = transport == null ? Transport.INHERIT : transport;
private final Set<String> _roles = roles == null || roles.isEmpty()
? Collections.emptySet()
: Collections.unmodifiableSet(roles);
private final Authorization _authorization = authorization == null
? (_roles.isEmpty() ? Authorization.INHERIT : Authorization.SPECIFIC_ROLE)
: authorization;
{
if (!_roles.isEmpty() && _authorization != Authorization.SPECIFIC_ROLE)
throw new IllegalStateException("Constraint with roles must be SPECIFIC_ROLE, not " + _authorization);
}
@Override
public String getName()
{
return _name;
}
@Override
public Transport getTransport()
{
return _transport;
}
@Override
public Authorization getAuthorization()
{
return _authorization;
}
@Override
public Set<String> getRoles()
{
return _roles;
}
@Override
public String toString()
{
return "Constraint@%x{%s,%s,%s,%s}".formatted(
hashCode(),
getName(),
getTransport(),
getAuthorization(),
getRoles());
}
};
}
}

View File

@ -0,0 +1,79 @@
//
// ========================================================================
// 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;
import java.util.Collection;
import org.eclipse.jetty.security.Authenticator.Configuration;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.security.authentication.SPNEGOAuthenticator;
import org.eclipse.jetty.security.authentication.SessionAuthentication;
import org.eclipse.jetty.security.authentication.SslClientCertAuthenticator;
import org.eclipse.jetty.security.internal.DeferredAuthenticationState;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.ssl.SslContextFactory;
/**
* The Default Authenticator Factory.
* Uses the {@link Configuration#getAuthenticationType()} to select an {@link Authenticator} from: <ul>
* <li>{@link BasicAuthenticator}</li>
* <li>{@link DigestAuthenticator}</li>
* <li>{@link FormAuthenticator}</li>
* <li>{@link SslClientCertAuthenticator}</li>
* <li>{@link SPNEGOAuthenticator}</li>
* </ul>
* All authenticators derived from {@link LoginAuthenticator} are
* wrapped with a {@link DeferredAuthenticationState}
* instance, which is used if authentication is not mandatory.
*
* The Authentications from the {@link FormAuthenticator} are always wrapped in a
* {@link SessionAuthentication}
* <p>
* If a {@link LoginService} has not been set on this factory, then
* the service is selected by searching the {@link Server#getBeans(Class)} results for
* a service that matches the realm name, else the first LoginService found is used.
*/
public class DefaultAuthenticatorFactory implements Authenticator.Factory
{
@Override
public Authenticator getAuthenticator(Server server, Context context, Configuration configuration)
{
String auth = configuration.getAuthenticationType();
Authenticator authenticator = null;
if (Authenticator.BASIC_AUTH.equalsIgnoreCase(auth))
authenticator = new BasicAuthenticator();
else if (Authenticator.DIGEST_AUTH.equalsIgnoreCase(auth))
authenticator = new DigestAuthenticator();
else if (Authenticator.FORM_AUTH.equalsIgnoreCase(auth))
authenticator = new FormAuthenticator();
else if (Authenticator.SPNEGO_AUTH.equalsIgnoreCase(auth))
authenticator = new SPNEGOAuthenticator();
else if (Authenticator.NEGOTIATE_AUTH.equalsIgnoreCase(auth)) // see Bug #377076
authenticator = new SPNEGOAuthenticator(Authenticator.NEGOTIATE_AUTH);
if (Authenticator.CERT_AUTH.equalsIgnoreCase(auth))
{
Collection<SslContextFactory> sslContextFactories = server.getBeans(SslContextFactory.class);
if (sslContextFactories.size() != 1)
throw new IllegalStateException("SslClientCertAuthenticator requires a single SslContextFactory instances.");
authenticator = new SslClientCertAuthenticator(sslContextFactories.iterator().next());
}
return authenticator;
}
}

View File

@ -0,0 +1,76 @@
//
// ========================================================================
// 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;
import java.security.Principal;
import javax.security.auth.Subject;
import org.eclipse.jetty.security.internal.DefaultUserIdentity;
import org.eclipse.jetty.security.internal.RoleRunAsToken;
/**
* The default {@link IdentityService}, which creates and uses {@link DefaultUserIdentity}s.
* The {@link #associate(UserIdentity, RunAsToken)} method ignores the
* {@code user}, but will associate the {@link RunAsToken} with the current thread
* until {@link Association#close()} is called.
*/
public class DefaultIdentityService implements IdentityService
{
private static final ThreadLocal<String> runAsRole = new ThreadLocal<>();
private static final Association NOOP = () -> {};
private static final Association CLEAR_RUN_AS = () -> runAsRole.set(null);
public static boolean isRoleAssociated(String role)
{
return role != null && role.equals(runAsRole.get());
}
@Override
public Association associate(UserIdentity user, RunAsToken runAsToken)
{
if (runAsToken instanceof RoleRunAsToken roleRunAsToken)
{
String oldAssociate = runAsRole.get();
runAsRole.set(roleRunAsToken.getRunAsRole());
if (oldAssociate == null)
return CLEAR_RUN_AS;
return () -> runAsRole.set(oldAssociate);
}
return NOOP;
}
@Override
public void onLogout(UserIdentity user)
{
runAsRole.set(null);
}
@Override
public RunAsToken newRunAsToken(String roleName)
{
return new RoleRunAsToken(roleName);
}
@Override
public UserIdentity getSystemUserIdentity()
{
return null;
}
@Override
public UserIdentity newUserIdentity(final Subject subject, final Principal userPrincipal, final String[] roles)
{
return new DefaultUserIdentity(subject, userPrincipal, roles);
}
}

View File

@ -11,9 +11,12 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;
import jakarta.servlet.ServletRequest;
import java.util.function.Function;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
/**
* LoginService implementation which always denies any attempt to login.
@ -27,7 +30,7 @@ public class EmptyLoginService implements LoginService
}
@Override
public UserIdentity login(String username, Object credentials, ServletRequest request)
public UserIdentity login(String username, Object credentials, Request request, Function<Boolean, Session> getOrCreateSession)
{
return null;
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security;
package org.eclipse.jetty.security;
import java.util.List;
@ -36,7 +36,7 @@ public class HashLoginService extends AbstractLoginService
private static final Logger LOG = LoggerFactory.getLogger(HashLoginService.class);
private Resource _config;
private int refreshInterval; // default is not to reload
private int _reloadInterval; // default is not to reload
private UserStore _userStore;
private boolean _userStoreAutoCreate = false;
@ -77,42 +77,42 @@ public class HashLoginService extends AbstractLoginService
* Is hot reload enabled on this user store
*
* @return true if hot reload was enabled before startup
* @deprecated use {@link #getRefreshInterval()}
* @deprecated use {@link #getReloadInterval()}
*/
@Deprecated
public boolean isHotReload()
{
return refreshInterval > 0;
return _reloadInterval > 0;
}
/**
* Enable Hot Reload of the Property File
*
* @param enable true to enable 1s refresh interval, false to disable
* @deprecated use {@link #setRefreshInterval(int)}
* @deprecated use {@link #setReloadInterval(int)}
*/
@Deprecated
public void setHotReload(boolean enable)
{
setRefreshInterval(enable ? 1 : 0);
setReloadInterval(enable ? 1 : 0);
}
/**
* @return the scan interval in seconds for reloading the property file.
*/
public int getRefreshInterval()
public int getReloadInterval()
{
return refreshInterval;
return _reloadInterval;
}
/**
* @param refreshIntervalSeconds Set the scan interval in seconds for reloading the property file.
* @param reloadIntervalSeconds Set the scan interval in seconds for reloading the property file.
*/
public void setRefreshInterval(int refreshIntervalSeconds)
public void setReloadInterval(int reloadIntervalSeconds)
{
if (isRunning())
throw new IllegalStateException("Cannot set while user store is running");
this.refreshInterval = refreshIntervalSeconds;
this._reloadInterval = reloadIntervalSeconds;
}
/**
@ -146,9 +146,9 @@ public class HashLoginService extends AbstractLoginService
if (_userStore == null)
{
if (LOG.isDebugEnabled())
LOG.debug("doStart: Starting new PropertyUserStore. PropertiesFile: {} refresh: {}s", _config, refreshInterval);
LOG.debug("doStart: Starting new PropertyUserStore. PropertiesFile: {} refresh: {}s", _config, _reloadInterval);
PropertyUserStore propertyUserStore = new PropertyUserStore();
propertyUserStore.setRefreshInterval(refreshInterval);
propertyUserStore.setReloadInterval(_reloadInterval);
propertyUserStore.setConfig(_config);
setUserStore(propertyUserStore);
_userStoreAutoCreate = true;

View File

@ -11,56 +11,32 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;
import java.io.Closeable;
import java.security.Principal;
import javax.security.auth.Subject;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
/**
* Associates UserIdentities from with threads and UserIdentity.Contexts.
*/
public interface IdentityService
{
static final String[] NO_ROLES = new String[]{};
/**
* Associate a user identity with the current thread.
* This is called with as a thread enters the
* {@link Handler#handle(Request, org.eclipse.jetty.server.Response, org.eclipse.jetty.util.Callback)}
* method and then again with a null argument as that call exits.
*
* @param user The current user or null for no user to associated.
* @return an object representing the previous associated state
*/
Object associate(UserIdentity user);
/**
* Disassociate the user identity from the current thread
* and restore previous identity.
*
* @param previous The opaque object returned from a call to {@link IdentityService#associate(UserIdentity)}
*/
void disassociate(Object previous);
/**
* Associate a runas Token with the current user and thread.
*
* @param user The UserIdentity
* @param token The runAsToken to associate.
* @return The previous runAsToken or null.
* @param runAsToken The runAsToken to associate, obtained from {@link #newRunAsToken(String)}, or null.
* @return A {@link Closeable} that, when closed, will disassociate the token and restore any prior associations.
*/
Object setRunAs(UserIdentity user, RunAsToken token);
Association associate(UserIdentity user, RunAsToken runAsToken);
/**
* Disassociate the current runAsToken from the thread
* and reassociate the previous token.
*
* @param token RUNAS returned from previous associateRunAs call
* Called to notify that a user has been logged out.
* The service may, among other actions, close any {@link Association} for the calling thread.
* @param user The user that has logged out
*/
void unsetRunAs(Object token);
void onLogout(UserIdentity user);
/**
* Create a new UserIdentity for use with this identity service.
@ -76,10 +52,27 @@ public interface IdentityService
/**
* Create a new RunAsToken from a runAsName (normally a role).
*
* @param runAsName Normally a role name
* @return A new immutable RunAsToken
* @param roleName a role name
* @return A token that can be passed to {@link #associate(UserIdentity, RunAsToken)}.
*/
RunAsToken newRunAsToken(String runAsName);
RunAsToken newRunAsToken(String roleName);
UserIdentity getSystemUserIdentity();
/**
* An association between an identity and the current thread that can be terminated by {@link #close()}.
* @see #associate(UserIdentity, RunAsToken)
*/
interface Association extends AutoCloseable
{
@Override
void close();
}
/**
* An opaque token created by {@link #newRunAsToken(String)} and used by {@link #associate(UserIdentity, RunAsToken)}
*/
interface RunAsToken
{
}
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security;
package org.eclipse.jetty.security;
import java.io.InputStream;
import java.sql.Connection;
@ -51,11 +51,9 @@ public class JDBCLoginService extends AbstractLoginService
protected Connection _con;
/**
* JDBCUserPrincipal
*
* A UserPrincipal with extra jdbc key info.
*/
public class JDBCUserPrincipal extends UserPrincipal
public static class JDBCUserPrincipal extends UserPrincipal
{
final int _userKey;

View File

@ -11,9 +11,14 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;
import jakarta.servlet.ServletRequest;
import java.security.Principal;
import java.util.function.Function;
import javax.security.auth.Subject;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
/**
* Login Service Interface.
@ -24,7 +29,6 @@ import jakarta.servlet.ServletRequest;
*/
public interface LoginService
{
/**
* @return Get the name of the login service (aka Realm name)
*/
@ -33,17 +37,38 @@ public interface LoginService
/**
* Login a user.
*
* @param username The user name
* @param credentials The users credentials
* @param request TODO
* @param username The username.
* @param credentials The users credentials.
* @param request The request or null
* @param getOrCreateSession function to retrieve or create a session.
* @return A UserIdentity if the credentials matched, otherwise null
*/
UserIdentity login(String username, Object credentials, ServletRequest request);
UserIdentity login(String username, Object credentials, Request request, Function<Boolean, Session> getOrCreateSession);
/**
* Get or create a {@link UserIdentity} that is not authenticated by the {@link LoginService}.
* Typically, this method is used when a user is separately authenticated, but the roles
* of this service are needed for authorization.
*
* @param subject The subject
* @param userPrincipal the userPrincipal
* @param create If true, the {@link #getIdentityService()} may be used to create a new {@link UserIdentity}.
* @return A {@link UserIdentity} or null.
*/
default UserIdentity getUserIdentity(Subject subject, Principal userPrincipal, boolean create)
{
UserIdentity userIdentity = login(userPrincipal.getName(), "", null, b -> null);
if (userIdentity != null)
return new RoleDelegateUserIdentity(subject, userPrincipal, userIdentity);
if (create && getIdentityService() != null)
return getIdentityService().newUserIdentity(subject, userPrincipal, new String[0]);
return null;
}
/**
* Validate a user identity.
* Validate that a UserIdentity previously created by a call
* to {@link #login(String, Object, ServletRequest)} is still valid.
* to {@link #login(String, Object, Request, Function)} is still valid.
*
* @param user The user to validate
* @return true if authentication has not been revoked for the user.

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security;
package org.eclipse.jetty.security;
import java.io.IOException;
import java.io.InputStream;
@ -52,7 +52,7 @@ public class PropertyUserStore extends UserStore implements Scanner.DiscreteList
protected Resource _configResource;
protected Scanner _scanner;
protected int _refreshInterval = 0;
protected int _reloadInterval = 0;
protected boolean _firstLoad = true; // true if first load, false from that point on
protected List<UserListener> _listeners;
@ -91,24 +91,24 @@ public class PropertyUserStore extends UserStore implements Scanner.DiscreteList
* Is hot reload enabled on this user store
*
* @return true if hot reload was enabled before startup
* @deprecated use {@link #getRefreshInterval()}
* @deprecated use {@link #getReloadInterval()}
*/
@Deprecated
public boolean isHotReload()
{
return getRefreshInterval() > 0;
return getReloadInterval() > 0;
}
/**
* Enable Hot Reload of the Property File
*
* @param enable true to enable to a 1 second scan, false to disable
* @deprecated use {@link #setRefreshInterval(int)}
* @deprecated use {@link #setReloadInterval(int)}
*/
@Deprecated
public void setHotReload(boolean enable)
{
setRefreshInterval(enable ? 1 : 0);
setReloadInterval(enable ? 1 : 0);
}
/**
@ -116,21 +116,21 @@ public class PropertyUserStore extends UserStore implements Scanner.DiscreteList
*
* @param scanSeconds the period in seconds to scan for property file changes, or 0 for no scanning
*/
public void setRefreshInterval(int scanSeconds)
public void setReloadInterval(int scanSeconds)
{
if (isRunning())
{
throw new IllegalStateException("Cannot set scan period while user store is running");
}
this._refreshInterval = scanSeconds;
this._reloadInterval = scanSeconds;
}
/**
* @return the period in seconds to scan for property file changes, or 0 for no scanning
*/
public int getRefreshInterval()
public int getReloadInterval()
{
return _refreshInterval;
return _reloadInterval;
}
@Override
@ -180,7 +180,7 @@ public class PropertyUserStore extends UserStore implements Scanner.DiscreteList
if (username.length() > 0)
{
String[] roleArray = IdentityService.NO_ROLES;
String[] roleArray = new String[0];
if (roles != null && roles.length() > 0)
roleArray = StringUtil.csvSplit(roles);
known.add(username);
@ -221,11 +221,11 @@ public class PropertyUserStore extends UserStore implements Scanner.DiscreteList
protected void doStart() throws Exception
{
Resource config = getConfig();
if (getRefreshInterval() > 0 && (config != null))
if (getReloadInterval() > 0 && (config != null))
{
_scanner = new Scanner(null, false);
_scanner.addFile(config.getPath());
_scanner.setScanInterval(_refreshInterval);
_scanner.setScanInterval(_reloadInterval);
_scanner.setReportExistingFilesOnStartup(false);
_scanner.addListener(this);
addBean(_scanner);

View File

@ -11,18 +11,18 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;
import java.security.Principal;
import javax.security.auth.Subject;
public class SpnegoUserIdentity implements UserIdentity
public class RoleDelegateUserIdentity implements UserIdentity
{
private final Subject _subject;
private final Principal _principal;
private final UserIdentity _roleDelegate;
public SpnegoUserIdentity(Subject subject, Principal principal, UserIdentity roleDelegate)
public RoleDelegateUserIdentity(Subject subject, Principal principal, UserIdentity roleDelegate)
{
_subject = subject;
_principal = principal;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security;
package org.eclipse.jetty.security;
import java.io.Serializable;
import java.security.Principal;

View File

@ -11,25 +11,25 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;
import java.io.Serializable;
import java.net.InetAddress;
import java.nio.file.Path;
import java.security.PrivilegedAction;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.Function;
import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.eclipse.jetty.ee10.servlet.security.authentication.AuthorizationService;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.security.SecurityUtils;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
@ -46,27 +46,28 @@ import org.slf4j.LoggerFactory;
* for example {@code HTTP/wonder.com}, using a {@code keyTab} file as the service principal
* credentials.</p>
* <p>Upon receiving an HTTP request, the server tries to authenticate the client
* calling {@link #login(String, Object, ServletRequest)} where the GSS APIs are used to
* calling {@link LoginService#login(String, Object, Request, Function)} where the GSS APIs are used to
* verify client tokens and (perhaps after a few round-trips) a {@code GSSContext} is
* established.</p>
*/
public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implements LoginService
public class SPNEGOLoginService extends ContainerLifeCycle implements LoginService
{
private static final Logger LOG = LoggerFactory.getLogger(ConfigurableSpnegoLoginService.class);
private static final Logger LOG = LoggerFactory.getLogger(SPNEGOLoginService.class);
private final GSSManager _gssManager = GSSManager.getInstance();
private final String _realm;
private final AuthorizationService _authorizationService;
private final LoginService _loginService;
private IdentityService _identityService = new DefaultIdentityService();
private String _serviceName;
private Path _keyTabPath;
private String _hostName;
private SpnegoContext _context;
private SPNEGOContext _context;
public ConfigurableSpnegoLoginService(String realm, AuthorizationService authorizationService)
public SPNEGOLoginService(String realm, LoginService loginService)
{
_realm = realm;
_authorizationService = authorizationService;
_loginService = loginService;
addBean(_loginService);
}
/**
@ -136,14 +137,14 @@ public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implement
_hostName = InetAddress.getLocalHost().getCanonicalHostName();
if (LOG.isDebugEnabled())
LOG.debug("Retrieving credentials for service {}/{}", getServiceName(), getHostName());
LoginContext loginContext = new LoginContext("", null, null, new SpnegoConfiguration());
LoginContext loginContext = new LoginContext("", null, null, new SPNEGOConfiguration());
loginContext.login();
Subject subject = loginContext.getSubject();
_context = Subject.doAs(subject, newSpnegoContext(subject));
_context = SecurityUtils.doAs(subject, newSpnegoContext(subject));
super.doStart();
}
private PrivilegedAction<SpnegoContext> newSpnegoContext(Subject subject)
private Callable<SPNEGOContext> newSpnegoContext(Subject subject)
{
return () ->
{
@ -154,7 +155,7 @@ public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implement
Oid spnegoOid = new Oid("1.3.6.1.5.5.2");
Oid[] mechanisms = new Oid[]{kerberosOid, spnegoOid};
GSSCredential serviceCredential = _gssManager.createCredential(serviceName, GSSCredential.DEFAULT_LIFETIME, mechanisms, GSSCredential.ACCEPT_ONLY);
SpnegoContext context = new SpnegoContext();
SPNEGOContext context = new SPNEGOContext();
context._subject = subject;
context._serviceCredential = serviceCredential;
return context;
@ -167,11 +168,10 @@ public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implement
}
@Override
public UserIdentity login(String username, Object credentials, ServletRequest req)
public UserIdentity login(String username, Object credentials, Request request, Function<Boolean, Session> getOrCreateSession)
{
Subject subject = _context._subject;
HttpServletRequest request = (HttpServletRequest)req;
HttpSession httpSession = request.getSession(false);
Session httpSession = getOrCreateSession.apply(false);
GSSContext gssContext = null;
if (httpSession != null)
{
@ -179,37 +179,36 @@ public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implement
gssContext = holder == null ? null : holder.gssContext;
}
if (gssContext == null)
gssContext = Subject.doAs(subject, newGSSContext());
gssContext = SecurityUtils.doAs(subject, newGSSContext());
byte[] input = Base64.getDecoder().decode((String)credentials);
byte[] output = Subject.doAs(_context._subject, acceptGSSContext(gssContext, input));
byte[] output = SecurityUtils.doAs(_context._subject, acceptGSSContext(gssContext, input));
String token = Base64.getEncoder().encodeToString(output);
String userName = toUserName(gssContext);
// Save the token in the principal so it can be sent in the response.
SpnegoUserPrincipal principal = new SpnegoUserPrincipal(userName, token);
SPNEGOUserPrincipal principal = new SPNEGOUserPrincipal(userName, token);
if (gssContext.isEstablished())
{
if (httpSession != null)
httpSession.removeAttribute(GSSContextHolder.ATTRIBUTE);
UserIdentity roles = _authorizationService.getUserIdentity(request, userName);
return new SpnegoUserIdentity(subject, principal, roles);
return _loginService.getUserIdentity(subject, principal, false);
}
else
{
// The GSS context is not established yet, save it into the HTTP session.
if (httpSession == null)
httpSession = request.getSession(true);
httpSession = getOrCreateSession.apply(true);
GSSContextHolder holder = new GSSContextHolder(gssContext);
httpSession.setAttribute(GSSContextHolder.ATTRIBUTE, holder);
// Return an unestablished UserIdentity.
return new SpnegoUserIdentity(subject, principal, null);
return new RoleDelegateUserIdentity(subject, principal, null);
}
}
private PrivilegedAction<GSSContext> newGSSContext()
private Callable<GSSContext> newGSSContext()
{
return () ->
{
@ -224,7 +223,7 @@ public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implement
};
}
private PrivilegedAction<byte[]> acceptGSSContext(GSSContext gssContext, byte[] token)
private Callable<byte[]> acceptGSSContext(GSSContext gssContext, byte[] token)
{
return () ->
{
@ -278,7 +277,7 @@ public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implement
{
}
private class SpnegoConfiguration extends Configuration
private class SPNEGOConfiguration extends Configuration
{
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name)
@ -304,7 +303,7 @@ public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implement
}
}
private static class SpnegoContext
private static class SPNEGOContext
{
private Subject _subject;
private GSSCredential _serviceCredential;

View File

@ -11,28 +11,29 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;
/**
* @version $Rev: 4701 $ $Date: 2009-03-03 13:01:26 +0100 (Tue, 03 Mar 2009) $
*/
public class RoleRunAsToken implements RunAsToken
import java.security.Principal;
public class SPNEGOUserPrincipal implements Principal
{
private final String _runAsRole;
private final String _name;
private final String _encodedToken;
public RoleRunAsToken(String runAsRole)
public SPNEGOUserPrincipal(String name, String encodedToken)
{
this._runAsRole = runAsRole;
}
public String getRunAsRole()
{
return _runAsRole;
_name = name;
_encodedToken = encodedToken;
}
@Override
public String toString()
public String getName()
{
return "RoleRunAsToken(" + _runAsRole + ")";
return _name;
}
public String getEncodedToken()
{
return _encodedToken;
}
}

View File

@ -0,0 +1,769 @@
//
// ========================================================================
// 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;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.PathSpecGroup;
import org.eclipse.jetty.security.Authenticator.Configuration;
import org.eclipse.jetty.security.Constraint.Authorization;
import org.eclipse.jetty.security.Constraint.Transport;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract SecurityHandler.
* <p>
* Select and apply an {@link Authenticator} to a request.
* <p>
* The Authenticator may either be directly set on the handler
* or it will be created during {@link #start()} with a call to
* either the default or set AuthenticatorFactory.
* <p>
* SecurityHandler has a set of parameters that are used by the
* Authentication.Configuration. At startup, any context init parameters
* that start with "org.eclipse.jetty.security." that do not have
* values in the SecurityHandler init parameters, are copied.
*/
public abstract class SecurityHandler extends Handler.Wrapper implements Configuration
{
public static String SESSION_AUTHENTICATED_ATTRIBUTE = "org.eclipse.jetty.security.sessionAuthenticated";
private static final Logger LOG = LoggerFactory.getLogger(SecurityHandler.class);
private static final List<Authenticator.Factory> __knownAuthenticatorFactories = new ArrayList<>();
private Authenticator _authenticator;
private Authenticator.Factory _authenticatorFactory;
private String _realmName;
private String _authenticationType;
private final Map<String, String> _parameters = new HashMap<>();
private LoginService _loginService;
private IdentityService _identityService;
private boolean _renewSession = true;
private AuthenticationState.Deferred _deferred;
static
{
TypeUtil.serviceStream(ServiceLoader.load(Authenticator.Factory.class))
.forEach(__knownAuthenticatorFactories::add);
__knownAuthenticatorFactories.add(new DefaultAuthenticatorFactory());
}
protected SecurityHandler()
{
addBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories));
}
/**
* Get the identityService.
*
* @return the identityService
*/
@Override
public IdentityService getIdentityService()
{
return _identityService;
}
/**
* Set the identityService.
*
* @param identityService the identityService to set
*/
public void setIdentityService(IdentityService identityService)
{
if (isStarted())
throw new IllegalStateException("Started");
updateBean(_identityService, identityService);
_identityService = identityService;
}
/**
* Get the loginService.
*
* @return the loginService
*/
@Override
public LoginService getLoginService()
{
return _loginService;
}
/**
* Set the loginService.
* If a {@link LoginService} is not set, or is set to null,
* then during {@link #doStart()}
* the {@link #findLoginService()} method is used to locate one.
*
* @param loginService the loginService to set
*/
public void setLoginService(LoginService loginService)
{
if (isStarted())
throw new IllegalStateException("Started");
updateBean(_loginService, loginService);
_loginService = loginService;
}
public Authenticator getAuthenticator()
{
return _authenticator;
}
/**
* Set the authenticator.
*
* @param authenticator the authenticator
* @throws IllegalStateException if the SecurityHandler is running
*/
public void setAuthenticator(Authenticator authenticator)
{
if (isStarted())
throw new IllegalStateException("Started");
updateBean(_authenticator, authenticator);
_authenticator = authenticator;
if (_authenticator != null)
_authenticationType = _authenticator.getAuthenticationType();
}
/**
* @return the authenticatorFactory
*/
public Authenticator.Factory getAuthenticatorFactory()
{
return _authenticatorFactory;
}
/**
* @param authenticatorFactory the authenticatorFactory to set
* @throws IllegalStateException if the SecurityHandler is running
*/
public void setAuthenticatorFactory(Authenticator.Factory authenticatorFactory)
{
if (isRunning())
throw new IllegalStateException("running");
updateBean(_authenticatorFactory, authenticatorFactory);
_authenticatorFactory = authenticatorFactory;
}
/**
* @return the list of discovered authenticatorFactories
*/
public List<Authenticator.Factory> getKnownAuthenticatorFactories()
{
return __knownAuthenticatorFactories;
}
/**
* @return the realmName
*/
@Override
public String getRealmName()
{
return _realmName;
}
/**
* @param realmName the realmName to set
* @throws IllegalStateException if the SecurityHandler is running
*/
public void setRealmName(String realmName)
{
if (isRunning())
throw new IllegalStateException("running");
_realmName = realmName;
}
/**
* @return the name of the Authenticator
*/
@Override
public String getAuthenticationType()
{
return _authenticationType;
}
/**
* @param authenticationType the name of the Authenticator to use
* @throws IllegalStateException if the SecurityHandler is running
*/
public void setAuthenticationType(String authenticationType)
{
if (isRunning())
throw new IllegalStateException("running");
_authenticationType = authenticationType;
if (_authenticator != null && !_authenticator.getAuthenticationType().equals(_authenticationType))
_authenticator = null;
}
@Override
public String getParameter(String key)
{
return _parameters.get(key);
}
@Override
public Set<String> getParameterNames()
{
return _parameters.keySet();
}
/**
* Set an authentication parameter for retrieval via {@link Configuration#getParameter(String)}
*
* @param key the key
* @param value the init value
* @return previous value
* @throws IllegalStateException if the SecurityHandler is started
*/
public String setParameter(String key, String value)
{
if (isStarted())
throw new IllegalStateException("started");
return _parameters.put(key, value);
}
/**
* Find an appropriate {@link LoginService} from the
* list returned by {@link org.eclipse.jetty.util.component.Container#getBeans(Class)}
* called on the result of {@link #getServer()}. A service is selected by:
* <ul>
* <li>if {@link #setRealmName(String)} has been called, the first service
* with a matching name is used</li>
* <li>if the list is size 1, that service is used</li>
* <li>otherwise no service is selected.</li>
* </ul>
* @return An appropriate {@link LoginService} or null
*/
protected LoginService findLoginService()
{
java.util.Collection<LoginService> list = getServer().getBeans(LoginService.class);
LoginService service = null;
String realm = getRealmName();
if (realm != null)
{
for (LoginService s : list)
{
if (s.getName() != null && s.getName().equals(realm))
{
service = s;
break;
}
}
}
else if (list.size() == 1)
service = list.iterator().next();
return service;
}
protected IdentityService findIdentityService()
{
return getServer().getBean(IdentityService.class);
}
@Override
protected void doStart()
throws Exception
{
// complicated resolution of login and identity service to handle
// many different ways these can be constructed and injected.
if (_loginService == null)
{
setLoginService(findLoginService());
if (_loginService != null)
unmanage(_loginService);
}
if (_identityService == null)
{
if (_loginService != null)
setIdentityService(_loginService.getIdentityService());
if (_identityService == null)
setIdentityService(findIdentityService());
if (_identityService == null)
{
setIdentityService(new DefaultIdentityService());
manage(_identityService);
}
else
unmanage(_identityService);
}
if (_loginService != null)
{
if (_loginService.getIdentityService() == null)
_loginService.setIdentityService(_identityService);
else if (_loginService.getIdentityService() != _identityService)
throw new IllegalStateException("LoginService has different IdentityService to " + this);
}
Context context = ContextHandler.getCurrentContext();
if (_authenticator == null)
{
// If someone has set an authenticator factory only use that, otherwise try the list of discovered factories.
if (_authenticatorFactory != null)
{
Authenticator authenticator = _authenticatorFactory.getAuthenticator(getServer(), context,
this);
if (authenticator != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Created authenticator {} with {}", authenticator, _authenticatorFactory);
setAuthenticator(authenticator);
}
}
else
{
for (Authenticator.Factory factory : getKnownAuthenticatorFactories())
{
Authenticator authenticator = factory.getAuthenticator(getServer(), context,
this);
if (authenticator != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Created authenticator {} with {}", authenticator, factory);
setAuthenticator(authenticator);
break;
}
}
}
}
if (_authenticator == null)
setAuthenticator(new Authenticator.NoOp());
if (_authenticator != null)
_authenticator.setConfiguration(this);
else if (_realmName != null)
{
LOG.warn("No Authenticator for {}", this);
throw new IllegalStateException("No Authenticator");
}
if (_authenticator instanceof LoginAuthenticator loginAuthenticator)
{
_deferred = AuthenticationState.defer(loginAuthenticator);
addBean(_deferred);
}
super.doStart();
}
@Override
protected void doStop() throws Exception
{
//if we discovered the services (rather than had them explicitly configured), remove them.
if (!isManaged(_identityService))
{
removeBean(_identityService);
_identityService = null;
}
if (!isManaged(_loginService))
{
removeBean(_loginService);
_loginService = null;
}
if (_deferred != null)
{
removeBean(_deferred);
_deferred = null;
}
super.doStop();
}
@Override
public boolean isSessionRenewedOnAuthentication()
{
return _renewSession;
}
/**
* Set renew the session on Authentication.
* <p>
* If set to true, then on authentication, the session associated with a reqeuest is invalidated and replaced with a new session.
*
* @param renew true to renew the authentication on session
* @see Configuration#isSessionRenewedOnAuthentication()
*/
public void setSessionRenewedOnAuthentication(boolean renew)
{
_renewSession = renew;
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
Handler next = getHandler();
if (next == null)
return false;
String pathInContext = Request.getPathInContext(request);
Constraint constraint = getConstraint(pathInContext, request);
if (LOG.isDebugEnabled())
LOG.debug("getConstraint({}) -> {}", pathInContext, constraint);
if (constraint == null)
constraint = Constraint.ALLOWED;
if (constraint.getAuthorization() == Authorization.FORBIDDEN)
{
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403);
return true;
}
// Check data constraints
if (Transport.SECURE.equals(constraint.getTransport()) && !request.isSecure())
{
redirectToSecure(request, response, callback);
return true;
}
// Determine Constraint.Authentication
Authorization constraintAuthorization = constraint.getAuthorization();
constraintAuthorization = _authenticator.getConstraintAuthentication(pathInContext, constraintAuthorization, request::getSession);
if (constraintAuthorization == Authorization.INHERIT)
constraintAuthorization = Authorization.ALLOWED;
if (LOG.isDebugEnabled())
LOG.debug("constraintAuthorization {}", constraintAuthorization);
boolean mustValidate = constraintAuthorization != Authorization.ALLOWED;
try
{
AuthenticationState authenticationState = mustValidate ? _authenticator.validateRequest(request, response, callback) : null;
if (LOG.isDebugEnabled())
LOG.debug("AuthenticationState {}", authenticationState);
if (authenticationState instanceof AuthenticationState.ResponseSent)
return true;
if (mustValidate && isNotAuthorized(constraint, authenticationState))
{
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "!authorized");
return true;
}
if (authenticationState == null)
authenticationState = _deferred;
AuthenticationState.setAuthenticationState(request, authenticationState);
IdentityService.Association association =
(authenticationState instanceof AuthenticationState.Succeeded user)
? _identityService.associate(user.getUserIdentity(), null) : null;
try
{
//process the request by other handlers
return next.handle(_authenticator.prepareRequest(request, authenticationState), response, callback);
}
finally
{
if (association == null && authenticationState instanceof AuthenticationState.Deferred deferred)
association = deferred.getAssociation();
if (association != null)
association.close();
}
}
catch (ServerAuthException e)
{
Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500, e.getMessage());
return true;
}
}
public static SecurityHandler getCurrentSecurityHandler()
{
ContextHandler contextHandler = ContextHandler.getCurrentContextHandler();
if (contextHandler != null)
return contextHandler.getDescendant(SecurityHandler.class);
return null;
}
protected abstract Constraint getConstraint(String pathInContext, Request request);
protected void redirectToSecure(Request request, Response response, Callback callback)
{
HttpConfiguration httpConfig = request.getConnectionMetaData().getHttpConfiguration();
if (httpConfig.getSecurePort() > 0)
{
//Redirect to secure port
String scheme = httpConfig.getSecureScheme();
int port = httpConfig.getSecurePort();
String url = URIUtil.newURI(scheme, Request.getServerName(request), port, request.getHttpURI().getPath(), request.getHttpURI().getQuery());
response.getHeaders().putLongField(HttpHeader.CONTENT_LENGTH, 0);
Response.sendRedirect(request, response, callback, HttpStatus.MOVED_TEMPORARILY_302, url, true);
}
else
{
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "!Secure");
}
}
protected boolean isNotAuthorized(Constraint constraint, AuthenticationState authenticationState)
{
UserIdentity userIdentity = authenticationState instanceof AuthenticationState.Succeeded user ? user.getUserIdentity() : null;
return switch (constraint.getAuthorization())
{
case FORBIDDEN, ALLOWED, INHERIT -> false;
case ANY_USER -> userIdentity == null || userIdentity.getUserPrincipal() == null;
case KNOWN_ROLE ->
{
if (userIdentity != null && userIdentity.getUserPrincipal() != null)
for (String role : getKnownRoles())
if (userIdentity.isUserInRole(role))
yield false;
yield true;
}
case SPECIFIC_ROLE ->
{
if (userIdentity != null && userIdentity.getUserPrincipal() != null)
for (String role : constraint.getRoles())
if (userIdentity.isUserInRole(role))
yield false;
yield true;
}
};
}
protected Set<String> getKnownRoles()
{
return Collections.emptySet();
}
public class NotChecked implements Principal
{
@Override
public String getName()
{
return null;
}
@Override
public String toString()
{
return "NOT CHECKED";
}
public SecurityHandler getSecurityHandler()
{
return SecurityHandler.this;
}
}
// TODO consider method mapping version
/**
* <p>A concrete implementation of {@link SecurityHandler} that uses a {@link PathMappings} to
* match request to a list of {@link Constraint}s, which are applied in the order of
* least significant to most significant.
* <p>
* An example of using this class is:
* <pre>
* SecurityHandler.PathMapped handler = new SecurityHandler.PathMapped();
* handler.put("/*", Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT);
* handler.put("", Constraint.ALLOWED);
* handler.put("/login", Constraint.ALLOWED);
* handler.put("*.png", Constraint.ANY_TRANSPORT);
* handler.put("/admin/*", Constraint.from("admin", "operator"));
* handler.put("/admin/super/*", Constraint.from("operator"));
* handler.put("/user/*", Constraint.ANY_USER);
* handler.put("*.xml", Constraint.FORBIDDEN);
* </pre>
* <p>
* When {@link #getConstraint(String, Request)} is called, any matching
* constraints are sorted into least to most significant with
* {@link #compare(PathSpec, PathSpec)}, resulting in the order in which
* {@link Constraint#combine(Constraint, Constraint)} will be applied.
* For example:
* </p>
* <ul>
* <li>{@code "/admin/index.html"} matches {@code "/*"} and {@code "/admin/*"}, resulting in a
* constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#SECURE}.</li>
* <li>{@code "/admin/logo.png"} matches {@code "/*"}, {@code "/admin/*"} and {@code "*.png"}, resulting in a
* constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#ANY}.</li>
* <li>{@code "/admin/config.xml"} matches {@code "/*"}, {@code "/admin/*"} and {@code "*.xml"}, resulting in a
* constraint of {@link Authorization#FORBIDDEN} and {@link Transport#SECURE}.</li>
* <li>{@code "/admin/super/index.html"} matches {@code "/*"}, {@code "/admin/*"} and {@code "/admin/super/*"}, resulting in a
* constraint of {@link Authorization#SPECIFIC_ROLE} and {@link Transport#SECURE}.</li>
* </ul>
*/
public static class PathMapped extends SecurityHandler implements Comparator<PathSpec>
{
private final PathMappings<Constraint> _mappings = new PathMappings<>();
private final Set<String> _knownRoles = new HashSet<>();
public PathMapped()
{
}
public Constraint put(String pathSpec, Constraint constraint)
{
return put(PathSpec.from(pathSpec), constraint);
}
public Constraint put(PathSpec pathSpec, Constraint constraint)
{
Set<String> roles = constraint.getRoles();
if (roles != null)
_knownRoles.addAll(roles);
return _mappings.put(pathSpec, constraint);
}
public Constraint get(PathSpec pathSpec)
{
return _mappings.get(pathSpec);
}
public Constraint remove(PathSpec pathSpec)
{
Constraint removed = _mappings.remove(pathSpec);
_knownRoles.clear();
_mappings.values().forEach(c ->
{
Set<String> roles = c.getRoles();
if (roles != null)
_knownRoles.addAll(roles);
});
return removed;
}
@Override
protected Constraint getConstraint(String pathInContext, Request request)
{
List<MappedResource<Constraint>> matches = _mappings.getMatches(pathInContext);
if (matches == null || matches.isEmpty())
return null;
if (matches.size() == 1)
return matches.get(0).getResource();
// apply from least specific to most specific
matches.sort(this::compare);
if (LOG.isDebugEnabled())
LOG.debug("getConstraint {} -> {}", pathInContext, matches);
Constraint constraint = null;
for (MappedResource<Constraint> c : matches)
constraint = Constraint.combine(constraint, c.getResource());
return constraint;
}
/**
* {@link Comparator} method to sort paths from least specific to most specific. Using
* the {@link #pathSpecGroupPrecedence(PathSpecGroup)} to rank different groups and
* {@link PathSpec#getSpecLength()} to rank within a group. This method may be overridden
* to provide different precedence between constraints.
* @param ps1 the first {@code PathSpec} to be compared.
* @param ps2 the second {@code PathSpec} to be compared.
* @return -1, 0 or 1
*/
@Override
public int compare(PathSpec ps1, PathSpec ps2)
{
PathSpecGroup g1 = ps1.getGroup();
PathSpecGroup g2 = ps2.getGroup();
if (g1.equals(g2))
return Integer.compare(ps1.getSpecLength(), ps2.getSpecLength());
return Integer.compare(pathSpecGroupPrecedence(g1), pathSpecGroupPrecedence(g2));
}
int compare(MappedResource<Constraint> c1, MappedResource<Constraint> c2)
{
PathSpecGroup g1 = c1.getPathSpec().getGroup();
PathSpecGroup g2 = c2.getPathSpec().getGroup();
int l1 = c1.getPathSpec().getSpecLength();
int l2 = c2.getPathSpec().getSpecLength();
if (g1.equals(g2))
return Integer.compare(l1, l2);
return Integer.compare(pathSpecGroupPrecedence(g1), pathSpecGroupPrecedence(g2));
}
/**
* Get the relative precedence of a {@link PathSpecGroup} used by {@link #compare(MappedResource, MappedResource)}
* to sort {@link Constraint}s. The precedence from most significant to least is:
* <ul>
* <li>{@link PathSpecGroup#EXACT}</li>
* <li>{@link PathSpecGroup#ROOT}</li>
* <li>{@link PathSpecGroup#SUFFIX_GLOB}</li>
* <li>{@link PathSpecGroup#MIDDLE_GLOB}</li>
* <li>{@link PathSpecGroup#PREFIX_GLOB}</li>
* <li>{@link PathSpecGroup#DEFAULT}</li>
* </ul>
* @param group The group to rank.
* @return An integer representing relative precedence between {@link PathSpecGroup}s.
*/
protected int pathSpecGroupPrecedence(PathSpecGroup group)
{
return switch (group)
{
case EXACT -> 5;
case ROOT -> 4;
case SUFFIX_GLOB -> 3;
case MIDDLE_GLOB -> 2;
case PREFIX_GLOB -> 1;
case DEFAULT -> 0;
};
}
@Override
protected Set<String> getKnownRoles()
{
return _knownRoles;
}
}
}

View File

@ -11,12 +11,12 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;
import java.security.GeneralSecurityException;
/**
* @version $Rev: 4466 $ $Date: 2009-02-10 23:42:54 +0100 (Tue, 10 Feb 2009) $
* A server specific Authentication or Authorization exception.
*/
public class ServerAuthException extends GeneralSecurityException
{

View File

@ -11,11 +11,13 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;
import java.security.Principal;
import javax.security.auth.Subject;
import org.eclipse.jetty.security.internal.DefaultUserIdentity;
/**
* User object that encapsulates user identity and operations such as run-as-role actions,
* checking isUserInRole and getUserPrincipal.
@ -25,7 +27,6 @@ import javax.security.auth.Subject;
*/
public interface UserIdentity
{
/**
* @return The user subject
*/
@ -46,34 +47,8 @@ public interface UserIdentity
*/
boolean isUserInRole(String role);
public interface UnauthenticatedUserIdentity extends UserIdentity
static UserIdentity from(Subject subject, Principal userPrincipal, String... roles)
{
return new DefaultUserIdentity(subject, userPrincipal, roles);
}
public static final UserIdentity UNAUTHENTICATED_IDENTITY = new UnauthenticatedUserIdentity()
{
@Override
public Subject getSubject()
{
return null;
}
@Override
public Principal getUserPrincipal()
{
return null;
}
@Override
public boolean isUserInRole(String role)
{
return false;
}
@Override
public String toString()
{
return "UNAUTHENTICATED";
}
};
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security;
package org.eclipse.jetty.security;
import java.io.Serializable;
import java.security.Principal;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security;
package org.eclipse.jetty.security;
import java.util.Arrays;
import java.util.Collections;
@ -34,16 +34,15 @@ public class UserStore extends ContainerLifeCycle
protected class User
{
protected UserPrincipal _userPrincipal;
protected List<RolePrincipal> _rolePrincipals = Collections.emptyList();
protected List<RolePrincipal> _rolePrincipals;
protected User(String username, Credential credential, String[] roles)
{
_userPrincipal = new UserPrincipal(username, credential);
_rolePrincipals = Collections.emptyList();
if (roles != null)
_rolePrincipals = Arrays.stream(roles).map(RolePrincipal::new).collect(Collectors.toList());
_rolePrincipals = (roles == null || roles.length == 0)
? Collections.emptyList()
: Arrays.stream(roles).map(RolePrincipal::new).collect(Collectors.toList());
}
protected UserPrincipal getUserPrincipal()

View File

@ -11,23 +11,21 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security.authentication;
package org.eclipse.jetty.security.authentication;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee10.servlet.security.Authentication;
import org.eclipse.jetty.ee10.servlet.security.Authentication.User;
import org.eclipse.jetty.ee10.servlet.security.ServerAuthException;
import org.eclipse.jetty.ee10.servlet.security.UserAuthentication;
import org.eclipse.jetty.ee10.servlet.security.UserIdentity;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.security.Constraint;
public class BasicAuthenticator extends LoginAuthenticator
{
@ -44,26 +42,23 @@ public class BasicAuthenticator extends LoginAuthenticator
}
@Override
public String getAuthMethod()
public String getAuthenticationType()
{
return Constraint.__BASIC_AUTH;
return Authenticator.BASIC_AUTH;
}
@Override
public Authentication validateRequest(Request req, Response res, Callback callback, boolean mandatory) throws ServerAuthException
public AuthenticationState validateRequest(Request req, Response res, Callback callback) throws ServerAuthException
{
String credentials = req.getHeaders().get(HttpHeader.AUTHORIZATION);
if (!mandatory)
return new DeferredAuthentication(this);
if (credentials != null)
{
int space = credentials.indexOf(' ');
if (space > 0)
{
String method = credentials.substring(0, space);
if ("basic".equalsIgnoreCase(method))
if ("Basic".equalsIgnoreCase(method))
{
credentials = credentials.substring(space + 1);
Charset charset = getCharset();
@ -76,29 +71,28 @@ public class BasicAuthenticator extends LoginAuthenticator
String username = credentials.substring(0, i);
String password = credentials.substring(i + 1);
UserIdentity user = login(username, password, req);
UserIdentity user = login(username, password, req, res);
if (user != null)
return new UserAuthentication(getAuthMethod(), user);
return new UserAuthenticationSucceeded(getAuthenticationType(), user);
}
}
}
}
if (DeferredAuthentication.isDeferred(res))
return Authentication.UNAUTHENTICATED;
if (res.isCommitted())
return null;
String value = "basic realm=\"" + _loginService.getName() + "\"";
String value = "Basic realm=\"" + _loginService.getName() + "\"";
Charset charset = getCharset();
if (charset != null)
value += ", charset=\"" + charset.name() + "\"";
res.getHeaders().put(HttpHeader.WWW_AUTHENTICATE.asString(), value);
Response.writeError(req, res, callback, HttpServletResponse.SC_UNAUTHORIZED);
return Authentication.SEND_CONTINUE;
Response.writeError(req, res, callback, HttpStatus.UNAUTHORIZED_401);
return AuthenticationState.CHALLENGE;
}
@Override
public boolean secureResponse(Request req, Response res, Callback callback, boolean mandatory, User validatedUser) throws ServerAuthException
public static String authorization(String user, String password)
{
return true;
return "Basic " + Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.ISO_8859_1));
}
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security.authentication;
package org.eclipse.jetty.security.authentication;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@ -24,28 +24,26 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee10.servlet.security.Authentication;
import org.eclipse.jetty.ee10.servlet.security.Authentication.User;
import org.eclipse.jetty.ee10.servlet.security.SecurityHandler;
import org.eclipse.jetty.ee10.servlet.security.ServerAuthException;
import org.eclipse.jetty.ee10.servlet.security.UserAuthentication;
import org.eclipse.jetty.ee10.servlet.security.UserIdentity;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The nonce max age in ms can be set with the {@link SecurityHandler#setInitParameter(String, String)}
* using the name "maxNonceAge". The nonce max count can be set with {@link SecurityHandler#setInitParameter(String, String)}
* The nonce max age in ms can be set with the {@link SecurityHandler#setParameter(String, String)}
* using the name "maxNonceAge". The nonce max count can be set with {@link SecurityHandler#setParameter(String, String)}
* using the name "maxNonceCount". When the age or count is exceeded, the nonce is considered stale.
*/
public class DigestAuthenticator extends LoginAuthenticator
@ -59,14 +57,14 @@ public class DigestAuthenticator extends LoginAuthenticator
private Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<>();
@Override
public void setConfiguration(AuthConfiguration configuration)
public void setConfiguration(Configuration configuration)
{
super.setConfiguration(configuration);
String mna = configuration.getInitParameter("maxNonceAge");
String mna = configuration.getParameter("maxNonceAge");
if (mna != null)
setMaxNonceAge(Long.parseLong(mna));
String mnc = configuration.getInitParameter("maxNonceCount");
String mnc = configuration.getParameter("maxNonceCount");
if (mnc != null)
setMaxNonceCount(Integer.parseInt(mnc));
}
@ -92,23 +90,14 @@ public class DigestAuthenticator extends LoginAuthenticator
}
@Override
public String getAuthMethod()
public String getAuthenticationType()
{
return Constraint.__DIGEST_AUTH;
return Authenticator.DIGEST_AUTH;
}
@Override
public boolean secureResponse(Request req, Response res, Callback callback, boolean mandatory, User validatedUser) throws ServerAuthException
public AuthenticationState validateRequest(Request req, Response res, Callback callback) throws ServerAuthException
{
return true;
}
@Override
public Authentication validateRequest(Request req, Response res, Callback callback, boolean mandatory) throws ServerAuthException
{
if (!mandatory)
return new DeferredAuthentication(this);
String credentials = req.getHeaders().get(HttpHeader.AUTHORIZATION);
boolean stale = false;
@ -168,17 +157,17 @@ public class DigestAuthenticator extends LoginAuthenticator
if (n > 0)
{
//UserIdentity user = _loginService.login(digest.username,digest);
UserIdentity user = login(digest.username, digest, req);
UserIdentity user = login(digest.username, digest, req, res);
if (user != null)
{
return new UserAuthentication(getAuthMethod(), user);
return new UserAuthenticationSucceeded(getAuthenticationType(), user);
}
}
else if (n == 0)
stale = true;
}
if (!DeferredAuthentication.isDeferred(res))
if (!AuthenticationState.Deferred.isDeferred(res))
{
String domain = req.getContext().getContextPath();
if (domain == null)
@ -189,21 +178,21 @@ public class DigestAuthenticator extends LoginAuthenticator
"\", algorithm=MD5" +
", qop=\"auth\"" +
", stale=" + stale);
Response.writeError(req, res, callback, HttpServletResponse.SC_UNAUTHORIZED);
Response.writeError(req, res, callback, HttpStatus.UNAUTHORIZED_401);
return Authentication.SEND_CONTINUE;
return AuthenticationState.CHALLENGE;
}
return Authentication.UNAUTHENTICATED;
return null;
}
@Override
public UserIdentity login(String username, Object credentials, Request request)
public UserIdentity login(String username, Object credentials, Request request, Response response)
{
Digest digest = (Digest)credentials;
if (!Objects.equals(digest.realm, _loginService.getName()))
return null;
return super.login(username, credentials, request);
return super.login(username, credentials, request, response);
}
public String newNonce(Request request)
@ -317,14 +306,15 @@ public class DigestAuthenticator extends LoginAuthenticator
try
{
// MD5 required by the specification
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] ha1;
if (credentials instanceof Credential.MD5)
if (credentials instanceof MD5)
{
// Credentials are already a MD5 digest - assume it's in
// form user:realm:password (we have no way to know since
// it's a digest, alright?)
ha1 = ((Credential.MD5)credentials).getDigest();
ha1 = ((MD5)credentials).getDigest();
}
else
{

View File

@ -0,0 +1,376 @@
//
// ========================================================================
// 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.authentication;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.AuthenticationState.Succeeded;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FORM Authenticator.
*
* <p>This authenticator implements form authentication will use dispatchers to
* the login page if the {@link #__FORM_DISPATCH} init parameter is set to true.
* Otherwise it will redirect.</p>
*
* <p>The form authenticator redirects unauthenticated requests to a log page
* which should use a form to gather username/password from the user and send them
* to the /j_security_check URI within the context. FormAuthentication uses
* {@link SessionAuthentication} to wrap Authentication results so that they
* are associated with the session.</p>
*/
public class FormAuthenticator extends LoginAuthenticator
{
private static final Logger LOG = LoggerFactory.getLogger(FormAuthenticator.class);
public static final String __FORM_LOGIN_PAGE = "org.eclipse.jetty.security.form_login_page";
public static final String __FORM_ERROR_PAGE = "org.eclipse.jetty.security.form_error_page";
public static final String __FORM_DISPATCH = "org.eclipse.jetty.security.dispatch";
public static final String __J_URI = "org.eclipse.jetty.security.form_URI";
public static final String __J_POST = "org.eclipse.jetty.security.form_POST";
public static final String __J_METHOD = "org.eclipse.jetty.security.form_METHOD";
public static final String __J_SECURITY_CHECK = "/j_security_check";
public static final String __J_USERNAME = "j_username";
public static final String __J_PASSWORD = "j_password";
private String _formErrorPage;
private String _formErrorPath;
private String _formLoginPage;
private String _formLoginPath;
private boolean _dispatch;
private boolean _alwaysSaveUri;
public FormAuthenticator()
{
}
public FormAuthenticator(String login, String error, boolean dispatch)
{
this();
if (login != null)
setLoginPage(login);
if (error != null)
setErrorPage(error);
_dispatch = dispatch;
}
/**
* If true, uris that cause a redirect to a login page will always
* be remembered. If false, only the first uri that leads to a login
* page redirect is remembered.
* See https://bugs.eclipse.org/bugs/show_bug.cgi?id=379909
*
* @param alwaysSave true to always save the uri
*/
public void setAlwaysSaveUri(boolean alwaysSave)
{
_alwaysSaveUri = alwaysSave;
}
public boolean getAlwaysSaveUri()
{
return _alwaysSaveUri;
}
@Override
public void setConfiguration(Configuration configuration)
{
super.setConfiguration(configuration);
String login = configuration.getParameter(FormAuthenticator.__FORM_LOGIN_PAGE);
if (login != null)
setLoginPage(login);
String error = configuration.getParameter(FormAuthenticator.__FORM_ERROR_PAGE);
if (error != null)
setErrorPage(error);
String dispatch = configuration.getParameter(FormAuthenticator.__FORM_DISPATCH);
_dispatch = dispatch == null ? _dispatch : Boolean.parseBoolean(dispatch);
}
@Override
public String getAuthenticationType()
{
return Authenticator.FORM_AUTH;
}
private void setLoginPage(String path)
{
if (!path.startsWith("/"))
{
LOG.warn("form-login-page must start with /");
path = "/" + path;
}
_formLoginPage = path;
_formLoginPath = path;
if (_formLoginPath.indexOf('?') > 0)
_formLoginPath = _formLoginPath.substring(0, _formLoginPath.indexOf('?'));
}
private void setErrorPage(String path)
{
if (path == null || path.trim().length() == 0)
{
_formErrorPath = null;
_formErrorPage = null;
}
else
{
if (!path.startsWith("/"))
{
LOG.warn("form-error-page must start with /");
path = "/" + path;
}
_formErrorPage = path;
_formErrorPath = path;
if (_formErrorPath.indexOf('?') > 0)
_formErrorPath = _formErrorPath.substring(0, _formErrorPath.indexOf('?'));
}
}
@Override
public UserIdentity login(String username, Object password, Request request, Response response)
{
UserIdentity user = super.login(username, password, request, response);
if (user != null)
{
Session session = request.getSession(true);
AuthenticationState cached = new SessionAuthentication(getAuthenticationType(), user, password);
session.setAttribute(SessionAuthentication.AUTHENTICATED_ATTRIBUTE, cached);
}
return user;
}
@Override
public void logout(Request request, Response response)
{
super.logout(request, response);
Session session = request.getSession(false);
if (session == null)
return;
//clean up session
session.removeAttribute(SessionAuthentication.AUTHENTICATED_ATTRIBUTE);
}
@Override
public Request prepareRequest(Request request, AuthenticationState authenticationState)
{
// if this is a request resulting from a redirect after auth is complete
// (ie its from a redirect to the original request uri) then due to
// browser handling of 302 redirects, the method may not be the same as
// that of the original request. Replace the method and original post
// params (if it was a post).
if (authenticationState instanceof Succeeded)
{
Session session = request.getSession(false);
HttpURI juri = (HttpURI)session.getAttribute(__J_URI);
HttpURI uri = request.getHttpURI();
if ((uri.equals(juri)))
{
session.removeAttribute(__J_URI);
Object post = session.removeAttribute(__J_POST);
if (post instanceof CompletableFuture<?> futureFields)
FormFields.set(request, (CompletableFuture<Fields>)futureFields);
String method = (String)session.removeAttribute(__J_METHOD);
if (method != null && request.getMethod().equals(method))
{
return new Request.Wrapper(request)
{
@Override
public String getMethod()
{
return method;
}
};
}
}
}
return request;
}
protected Fields getParameters(Request request)
{
try
{
Fields queryFields = Request.extractQueryParameters(request);
Fields formFields = FormFields.from(request).get();
return Fields.combine(queryFields, formFields);
}
catch (InterruptedException | ExecutionException e)
{
throw new RuntimeException(e);
}
}
protected String encodeURL(String url, Request request)
{
Session session = request.getSession(false);
if (session == null)
return url;
return session.encodeURI(request, url, request.getHeaders().contains(HttpHeader.COOKIE));
}
@Override
public Constraint.Authorization getConstraintAuthentication(String pathInContext, Constraint.Authorization existing, Function<Boolean, Session> getSession)
{
if (isJSecurityCheck(pathInContext))
return Constraint.Authorization.ANY_USER;
if (isLoginOrErrorPage(pathInContext))
return Constraint.Authorization.ALLOWED;
return existing;
}
@Override
public AuthenticationState validateRequest(Request request, Response response, Callback callback) throws ServerAuthException
{
String pathInContext = Request.getPathInContext(request);
boolean jSecurityCheck = isJSecurityCheck(pathInContext);
// Handle a request for authentication.
if (jSecurityCheck)
{
Fields parameters = getParameters(request);
final String username = parameters.getValue(__J_USERNAME);
final String password = parameters.getValue(__J_PASSWORD);
UserIdentity user = login(username, password, request, response);
LOG.debug("jsecuritycheck {} {}", username, user);
if (user != null)
{
// Redirect to original request
Session session = request.getSession(false);
HttpURI savedURI = (HttpURI)session.getAttribute(__J_URI);
String originalURI = savedURI != null ? savedURI.asString() : Request.getContextPath(request);
if (originalURI == null)
originalURI = "/";
UserAuthenticationSent formAuth = new UserAuthenticationSent(getAuthenticationType(), user);
Response.sendRedirect(request, response, callback, encodeURL(originalURI, request), true);
return formAuth;
}
// not authenticated
if (_formErrorPage == null)
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403);
else
Response.sendRedirect(request, response, callback, encodeURL(URIUtil.addPaths(request.getContext().getContextPath(), _formErrorPage), request), true);
return AuthenticationState.SEND_FAILURE;
}
// Look for cached authentication
Session session = request.getSession(false);
AuthenticationState authenticationState = session == null ? null : (AuthenticationState)session.getAttribute(SessionAuthentication.AUTHENTICATED_ATTRIBUTE);
if (LOG.isDebugEnabled())
LOG.debug("auth {}", authenticationState);
// Has authentication been revoked?
if (authenticationState instanceof Succeeded succeeded && _loginService != null && !_loginService.validate(succeeded.getUserIdentity()))
{
if (LOG.isDebugEnabled())
LOG.debug("auth revoked {}", authenticationState);
session.removeAttribute(SessionAuthentication.AUTHENTICATED_ATTRIBUTE);
authenticationState = null;
}
if (authenticationState != null)
return authenticationState;
// if we can't send challenge
if (response.isCommitted())
{
LOG.debug("auth deferred {}", session == null ? null : session.getId());
return null;
}
// remember the current URI
session = (session != null ? session : request.getSession(true));
synchronized (session)
{
// But only if it is not set already, or we save every uri that leads to a login form redirect
if (session.getAttribute(__J_URI) == null || _alwaysSaveUri)
{
HttpURI juri = request.getHttpURI();
session.setAttribute(__J_URI, juri.asImmutable());
if (!HttpMethod.GET.is(request.getMethod()))
session.setAttribute(__J_METHOD, request.getMethod());
if (HttpMethod.POST.is(request.getMethod()))
{
try
{
CompletableFuture<Fields> futureFields = FormFields.from(request);
futureFields.get();
session.setAttribute(__J_POST, futureFields);
}
catch (ExecutionException e)
{
throw new ServerAuthException(e.getCause());
}
catch (InterruptedException e)
{
throw new ServerAuthException(e);
}
}
}
}
// send the challenge
if (LOG.isDebugEnabled())
LOG.debug("challenge {}->{}", session.getId(), _formLoginPage);
Response.sendRedirect(request, response, callback, encodeURL(URIUtil.addPaths(request.getContext().getContextPath(), _formLoginPage), request), true);
return AuthenticationState.CHALLENGE;
}
public boolean isJSecurityCheck(String uri)
{
int jsc = uri.indexOf(__J_SECURITY_CHECK);
if (jsc < 0)
return false;
int e = jsc + __J_SECURITY_CHECK.length();
if (e == uri.length())
return true;
char c = uri.charAt(e);
return c == ';' || c == '#' || c == '/' || c == '?';
}
public boolean isLoginOrErrorPage(String pathInContext)
{
return pathInContext != null && (pathInContext.equals(_formErrorPath) || pathInContext.equals(_formLoginPath));
}
}

View File

@ -0,0 +1,247 @@
//
// ========================================================================
// 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.authentication;
import java.io.Serial;
import java.io.Serializable;
import java.util.function.Function;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class LoginAuthenticator implements Authenticator
{
private static final Logger LOG = LoggerFactory.getLogger(LoginAuthenticator.class);
protected LoginService _loginService;
protected IdentityService _identityService;
private boolean _renewSession;
protected LoginAuthenticator()
{
}
/**
* If the UserIdentity returned from
* {@link LoginService#login(String, Object, Request, Function)} is not null, it
* is assumed that the user is fully authenticated and we need to change the session id to prevent
* session fixation vulnerability. If the UserIdentity is not necessarily fully
* authenticated, then subclasses must override this method and
* determine when the UserIdentity IS fully authenticated and renew the session id.
*
* @param username the username of the client to be authenticated
* @param password the user's credential
* @param request the inbound request that needs authentication
*/
public UserIdentity login(String username, Object password, Request request, Response response)
{
UserIdentity user = _loginService.login(username, password, request, request::getSession);
if (LOG.isDebugEnabled())
LOG.debug("{}.login {}", this, user);
if (user != null)
{
renewSession(request, response);
return user;
}
return null;
}
public void logout(Request request, Response response)
{
Session session = request.getSession(false);
if (LOG.isDebugEnabled())
LOG.debug("{}.logout {}", this, session);
if (session == null)
return;
session.removeAttribute(SecurityHandler.SESSION_AUTHENTICATED_ATTRIBUTE);
}
@Override
public void setConfiguration(Configuration configuration)
{
_loginService = configuration.getLoginService();
if (_loginService == null)
throw new IllegalStateException("No LoginService for " + this + " in " + configuration);
_identityService = configuration.getIdentityService();
if (_identityService == null)
throw new IllegalStateException("No IdentityService for " + this + " in " + configuration);
_renewSession = configuration.isSessionRenewedOnAuthentication();
}
public LoginService getLoginService()
{
return _loginService;
}
/**
* Change the session id.
* The session is changed to a new instance with a new ID if and only if:<ul>
* <li>A session exists.
* <li>The {@link Configuration#isSessionRenewedOnAuthentication()} returns true.
* <li>The session ID has been given to unauthenticated responses
* </ul>
*
* @param httpRequest the request
* @param httpResponse the response
* @return The new session.
*/
protected Session renewSession(Request httpRequest, Response httpResponse)
{
Session session = httpRequest.getSession(false);
if (_renewSession && session != null)
{
synchronized (session)
{
//if we should renew sessions, and there is an existing session that may have been seen by non-authenticated users
//(indicated by SESSION_SECURED not being set on the session) then we should change id
if (session.getAttribute(SecurityHandler.SESSION_AUTHENTICATED_ATTRIBUTE) != Boolean.TRUE)
{
session.setAttribute(SecurityHandler.SESSION_AUTHENTICATED_ATTRIBUTE, Boolean.TRUE);
session.renewId(httpRequest, httpResponse);
return session;
}
}
}
return session;
}
/**
* Base class for representing a successful authentication state.
*/
public static class UserAuthenticationSucceeded implements AuthenticationState.Succeeded, Serializable
{
@Serial
private static final long serialVersionUID = -6290411814232723403L;
protected String _authenticationType;
protected transient UserIdentity _userIdentity;
public UserAuthenticationSucceeded(String authenticationType, UserIdentity userIdentity)
{
_authenticationType = authenticationType;
_userIdentity = userIdentity;
}
@Override
public String getAuthenticationType()
{
return _authenticationType;
}
@Override
public UserIdentity getUserIdentity()
{
return _userIdentity;
}
@Override
public boolean isUserInRole(String role)
{
return _userIdentity.isUserInRole(role);
}
@Override
public void logout(Request request, Response response)
{
SecurityHandler security = SecurityHandler.getCurrentSecurityHandler();
if (security != null)
{
LoginService loginService = security.getLoginService();
if (loginService != null)
loginService.logout(((Succeeded)this).getUserIdentity());
IdentityService identityService = security.getIdentityService();
if (identityService != null)
identityService.onLogout(((Succeeded)this).getUserIdentity());
Authenticator authenticator = security.getAuthenticator();
AuthenticationState authenticationState = null;
if (authenticator instanceof LoginAuthenticator loginAuthenticator)
{
((LoginAuthenticator)authenticator).logout(request, response);
authenticationState = new LoginAuthenticator.LoggedOutAuthentication(loginAuthenticator);
}
AuthenticationState.setAuthenticationState(request, authenticationState);
}
}
@Override
public String toString()
{
return "%s@%x{%s,%s}".formatted(getClass().getSimpleName(), hashCode(), getAuthenticationType(), getUserIdentity());
}
}
/**
* This Authentication represents a just completed authentication, that has sent a response, typically to
* redirect the client to the original request target..
*/
public static class UserAuthenticationSent extends UserAuthenticationSucceeded implements AuthenticationState.ResponseSent
{
public UserAuthenticationSent(String method, UserIdentity userIdentity)
{
super(method, userIdentity);
}
}
public static class LoggedOutAuthentication implements AuthenticationState.Deferred
{
@Override
public Succeeded login(String username, Object password, Request request, Response response)
{
return _delegate.login(username, password, request, response);
}
@Override
public void logout(Request request, Response response)
{
_delegate.logout(request, response);
}
@Override
public IdentityService.Association getAssociation()
{
return _delegate.getAssociation();
}
private final Deferred _delegate;
public LoggedOutAuthentication(LoginAuthenticator authenticator)
{
_delegate = AuthenticationState.defer(authenticator);
}
@Override
public Succeeded authenticate(Request request)
{
return null;
}
@Override
public AuthenticationState authenticate(Request request, Response response, Callback callback)
{
return null;
}
}
}

View File

@ -11,29 +11,26 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security.authentication;
package org.eclipse.jetty.security.authentication;
import java.io.Serializable;
import java.time.Duration;
import java.time.Instant;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.eclipse.jetty.ee10.servlet.ServletContextRequest;
import org.eclipse.jetty.ee10.servlet.security.Authentication;
import org.eclipse.jetty.ee10.servlet.security.Authentication.User;
import org.eclipse.jetty.ee10.servlet.security.ConfigurableSpnegoLoginService;
import org.eclipse.jetty.ee10.servlet.security.ServerAuthException;
import org.eclipse.jetty.ee10.servlet.security.SpnegoUserIdentity;
import org.eclipse.jetty.ee10.servlet.security.SpnegoUserPrincipal;
import org.eclipse.jetty.ee10.servlet.security.UserAuthentication;
import org.eclipse.jetty.ee10.servlet.security.UserIdentity;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.RoleDelegateUserIdentity;
import org.eclipse.jetty.security.SPNEGOLoginService;
import org.eclipse.jetty.security.SPNEGOUserPrincipal;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.security.Constraint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -43,34 +40,34 @@ import org.slf4j.LoggerFactory;
* {@link #getAuthenticationDuration() duration} using the HTTP session; this avoids
* that the client is asked to authenticate for every request.</p>
*
* @see ConfigurableSpnegoLoginService
* @see SPNEGOLoginService
*/
public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator
public class SPNEGOAuthenticator extends LoginAuthenticator
{
private static final Logger LOG = LoggerFactory.getLogger(ConfigurableSpnegoAuthenticator.class);
private static final Logger LOG = LoggerFactory.getLogger(SPNEGOAuthenticator.class);
private final String _authMethod;
private final String _type;
private Duration _authenticationDuration = Duration.ofNanos(-1);
public ConfigurableSpnegoAuthenticator()
public SPNEGOAuthenticator()
{
this(Constraint.__SPNEGO_AUTH);
this(Authenticator.SPNEGO_AUTH);
}
/**
* Allow for a custom authMethod value to be set for instances where SPNEGO may not be appropriate
* Allow for a custom name value to be set for instances where SPNEGO may not be appropriate
*
* @param authMethod the auth method
* @param type the authenticator name
*/
public ConfigurableSpnegoAuthenticator(String authMethod)
public SPNEGOAuthenticator(String type)
{
_authMethod = authMethod;
_type = type;
}
@Override
public String getAuthMethod()
public String getAuthenticationType()
{
return _authMethod;
return _type;
}
/**
@ -99,42 +96,36 @@ public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator
* renew the session for any of the intermediate request/response handshakes.
*/
@Override
public UserIdentity login(String username, Object password, Request request)
public UserIdentity login(String username, Object password, Request request, Response response)
{
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
SpnegoUserIdentity user = (SpnegoUserIdentity)_loginService.login(username, password, servletContextRequest.getServletApiRequest());
RoleDelegateUserIdentity user = (RoleDelegateUserIdentity)_loginService.login(username, password, request, request::getSession);
if (user != null && user.isEstablished())
{
renewSession(servletContextRequest.getServletApiRequest(), servletContextRequest.getResponse().getServletApiResponse());
renewSession(request, response);
}
return user;
}
@Override
public Authentication validateRequest(Request req, Response res, Callback callback, boolean mandatory) throws ServerAuthException
public AuthenticationState validateRequest(Request req, Response res, Callback callback) throws ServerAuthException
{
if (!mandatory)
return new DeferredAuthentication(this);
ServletContextRequest servletContextRequest = Request.as(req, ServletContextRequest.class);
String header = req.getHeaders().get(HttpHeader.AUTHORIZATION);
String spnegoToken = getSpnegoToken(header);
HttpSession httpSession = servletContextRequest.getServletApiRequest().getSession(false);
Session httpSession = req.getSession(false);
// We have a token from the client, so run the login.
if (header != null && spnegoToken != null)
{
SpnegoUserIdentity identity = (SpnegoUserIdentity)login(null, spnegoToken, req);
RoleDelegateUserIdentity identity = (RoleDelegateUserIdentity)login(null, spnegoToken, req, res);
if (identity.isEstablished())
{
if (!DeferredAuthentication.isDeferred(res))
if (!AuthenticationState.Deferred.isDeferred(res))
{
if (LOG.isDebugEnabled())
LOG.debug("Sending final token");
// Send to the client the final token so that the
// client can establish the GSS context with the server.
SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal();
SPNEGOUserPrincipal principal = (SPNEGOUserPrincipal)identity.getUserPrincipal();
setSpnegoToken(res, principal.getEncodedToken());
}
@ -142,21 +133,20 @@ public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator
if (!authnDuration.isNegative())
{
if (httpSession == null)
httpSession = servletContextRequest.getServletApiRequest().getSession(true);
httpSession = req.getSession(true);
httpSession.setAttribute(UserIdentityHolder.ATTRIBUTE, new UserIdentityHolder(identity));
}
return new UserAuthentication(getAuthMethod(), identity);
return new UserAuthenticationSucceeded(getAuthenticationType(), identity);
}
else
{
if (DeferredAuthentication.isDeferred(res))
return Authentication.UNAUTHENTICATED;
if (AuthenticationState.Deferred.isDeferred(res))
return null;
if (LOG.isDebugEnabled())
LOG.debug("Sending intermediate challenge");
SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal();
SPNEGOUserPrincipal principal = (SPNEGOUserPrincipal)identity.getUserPrincipal();
sendChallenge(req, res, callback, principal.getEncodedToken());
return Authentication.SEND_CONTINUE;
return AuthenticationState.CHALLENGE;
}
}
// No token from the client; check if the client has logged in
@ -176,25 +166,25 @@ public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator
// Allow non-GET requests even if they're expired, so that
// the client does not need to send the request content again.
if (!expired || !HttpMethod.GET.is(req.getMethod()))
return new UserAuthentication(getAuthMethod(), identity);
return new UserAuthenticationSucceeded(getAuthenticationType(), identity);
}
}
}
}
if (DeferredAuthentication.isDeferred(res))
return Authentication.UNAUTHENTICATED;
if (AuthenticationState.Deferred.isDeferred(res))
return null;
if (LOG.isDebugEnabled())
LOG.debug("Sending initial challenge");
sendChallenge(req, res, callback, null);
return Authentication.SEND_CONTINUE;
return AuthenticationState.CHALLENGE;
}
private void sendChallenge(Request req, Response res, Callback callback, String token) throws ServerAuthException
private void sendChallenge(Request req, Response res, Callback callback, String token)
{
setSpnegoToken(res, token);
Response.writeError(req, res, callback, HttpServletResponse.SC_UNAUTHORIZED);
Response.writeError(req, res, callback, HttpStatus.UNAUTHORIZED_401);
}
private void setSpnegoToken(Response response, String token)
@ -215,12 +205,6 @@ public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator
return null;
}
@Override
public boolean secureResponse(Request request, Response response, Callback callback, boolean mandatory, User validatedUser)
{
return true;
}
private static class UserIdentityHolder implements Serializable
{
private static final String ATTRIBUTE = UserIdentityHolder.class.getName();

View File

@ -11,21 +11,17 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security.authentication;
package org.eclipse.jetty.security.authentication;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionActivationListener;
import jakarta.servlet.http.HttpSessionBindingListener;
import jakarta.servlet.http.HttpSessionEvent;
import org.eclipse.jetty.ee10.servlet.security.AbstractUserAuthentication;
import org.eclipse.jetty.ee10.servlet.security.Authenticator;
import org.eclipse.jetty.ee10.servlet.security.LoginService;
import org.eclipse.jetty.ee10.servlet.security.SecurityHandler;
import org.eclipse.jetty.ee10.servlet.security.UserIdentity;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -36,18 +32,18 @@ import org.slf4j.LoggerFactory;
* of Authenticator, the Authenticator stashes a SessionAuthentication
* into an HttpSession to remember that the user is authenticated.
*/
public class SessionAuthentication extends AbstractUserAuthentication
implements Serializable, HttpSessionActivationListener, HttpSessionBindingListener
public class SessionAuthentication extends LoginAuthenticator.UserAuthenticationSucceeded
implements Serializable, Session.ValueListener
{
private static final Logger LOG = LoggerFactory.getLogger(SessionAuthentication.class);
private static final long serialVersionUID = -4643200685888258706L;
public static final String __J_AUTHENTICATED = "org.eclipse.jetty.security.UserIdentity";
public static final String AUTHENTICATED_ATTRIBUTE = "org.eclipse.jetty.security.UserIdentity";
private final String _name;
private final Object _credentials;
private transient HttpSession _session;
private Session _session;
public SessionAuthentication(String method, UserIdentity userIdentity, Object credentials)
{
@ -91,7 +87,7 @@ public class SessionAuthentication extends AbstractUserAuthentication
return;
}
_userIdentity = loginService.login(_name, _credentials, null);
_userIdentity = loginService.login(_name, _credentials, null, null);
LOG.debug("Deserialized and relogged in {}", this);
}
@ -102,16 +98,13 @@ public class SessionAuthentication extends AbstractUserAuthentication
}
@Override
public void sessionWillPassivate(HttpSessionEvent se)
public void onSessionActivation(Session session)
{
_session = session;
}
@Override
public void sessionDidActivate(HttpSessionEvent se)
public void onSessionPassivation(Session session)
{
if (_session == null)
{
_session = se.getSession();
}
}
}

View File

@ -11,24 +11,23 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security.authentication;
package org.eclipse.jetty.security.authentication;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Objects;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee10.servlet.security.Authentication;
import org.eclipse.jetty.ee10.servlet.security.Authentication.User;
import org.eclipse.jetty.ee10.servlet.security.Authenticator;
import org.eclipse.jetty.ee10.servlet.security.ServerAuthException;
import org.eclipse.jetty.ee10.servlet.security.UserAuthentication;
import org.eclipse.jetty.ee10.servlet.security.UserIdentity;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.SecureRequestCustomizer.SslSessionData;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
/**
@ -49,59 +48,72 @@ public class SslClientCertAuthenticator extends LoginAuthenticator
}
@Override
public String getAuthMethod()
public String getAuthenticationType()
{
return Constraint.__CERT_AUTH;
return Authenticator.CERT_AUTH;
}
@Override
public Authentication validateRequest(Request req, Response res, Callback callback, boolean mandatory) throws ServerAuthException
public AuthenticationState validateRequest(Request req, Response res, Callback callback) throws ServerAuthException
{
if (!mandatory)
return new DeferredAuthentication(this);
X509Certificate[] certs = (X509Certificate[])req.getAttribute("jakarta.servlet.request.X509Certificate");
SslSessionData sslSessionData = (SslSessionData)req.getAttribute(SecureRequestCustomizer.DEFAULT_SSL_SESSION_DATA_ATTRIBUTE);
if (sslSessionData == null)
{
Response.writeError(req, res, callback, HttpStatus.FORBIDDEN_403);
return AuthenticationState.SEND_FAILURE;
}
X509Certificate[] certs = sslSessionData.peerCertificates();
try
{
// Need certificates.
if (certs != null && certs.length > 0)
{
if (validateCerts)
{
sslContextFactory.validateCerts(certs);
}
for (X509Certificate cert : certs)
{
if (cert == null)
continue;
Principal principal = cert.getSubjectX500Principal();
Principal principal = cert.getSubjectDN();
if (principal == null)
principal = cert.getIssuerX500Principal();
String username = principal == null ? "clientcert" : principal.getName();
principal = cert.getIssuerDN();
final String username = principal == null ? "clientcert" : principal.getName();
UserIdentity user = login(username, "", req);
UserIdentity user = login(username, "", req, res);
if (user != null)
return new UserAuthentication(getAuthMethod(), user);
{
return new UserAuthenticationSucceeded(getAuthenticationType(), user);
}
// try with null password
user = login(username, null, req);
user = login(username, null, req, res);
if (user != null)
return new UserAuthentication(getAuthMethod(), user);
{
return new UserAuthenticationSucceeded(getAuthenticationType(), user);
}
// try with certs sig against login service as previous behaviour
char[] credential = Base64.getEncoder().encodeToString(cert.getSignature()).toCharArray();
user = login(username, credential, req);
final char[] credential = Base64.getEncoder().encodeToString(cert.getSignature()).toCharArray();
user = login(username, credential, req, res);
if (user != null)
return new UserAuthentication(getAuthMethod(), user);
{
return new UserAuthenticationSucceeded(getAuthenticationType(), user);
}
}
}
if (!DeferredAuthentication.isDeferred(res))
if (!AuthenticationState.Deferred.isDeferred(res))
{
Response.writeError(req, res, callback, HttpServletResponse.SC_FORBIDDEN);
return Authentication.SEND_FAILURE;
Response.writeError(req, res, callback, HttpStatus.FORBIDDEN_403);
return AuthenticationState.SEND_FAILURE;
}
return Authentication.UNAUTHENTICATED;
return null;
}
catch (Exception e)
{
@ -109,12 +121,6 @@ public class SslClientCertAuthenticator extends LoginAuthenticator
}
}
@Override
public boolean secureResponse(Request req, Response res, Callback callback, boolean mandatory, User validatedUser) throws ServerAuthException
{
return true;
}
/**
* @return true if SSL certificate has to be validated.
*/

View File

@ -14,5 +14,5 @@
/**
* Jetty Security : Authenticators and Callbacks
*/
package org.eclipse.jetty.ee10.servlet.security.authentication;
package org.eclipse.jetty.security.authentication;

View File

@ -11,11 +11,14 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security.internal;
import java.security.Principal;
import javax.security.auth.Subject;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.UserIdentity;
/**
* The default implementation of UserIdentity.
*/
@ -47,9 +50,8 @@ public class DefaultUserIdentity implements UserIdentity
@Override
public boolean isUserInRole(String role)
{
//Servlet Spec 3.1, pg 125
if ("*".equals(role))
return false;
if (DefaultIdentityService.isRoleAssociated(role))
return true;
for (String r : _roles)
{

View File

@ -11,35 +11,33 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet.security.authentication;
package org.eclipse.jetty.security.internal;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import org.eclipse.jetty.ee10.servlet.security.Authentication;
import org.eclipse.jetty.ee10.servlet.security.IdentityService;
import org.eclipse.jetty.ee10.servlet.security.LoggedOutAuthentication;
import org.eclipse.jetty.ee10.servlet.security.LoginService;
import org.eclipse.jetty.ee10.servlet.security.SecurityHandler;
import org.eclipse.jetty.ee10.servlet.security.ServerAuthException;
import org.eclipse.jetty.ee10.servlet.security.UserAuthentication;
import org.eclipse.jetty.ee10.servlet.security.UserIdentity;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpFields.Mutable;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DeferredAuthentication implements Authentication.Deferred
public class DeferredAuthenticationState implements AuthenticationState.Deferred
{
private static final Logger LOG = LoggerFactory.getLogger(DeferredAuthentication.class);
private static final Logger LOG = LoggerFactory.getLogger(DeferredAuthenticationState.class);
protected final LoginAuthenticator _authenticator;
private Object _previousAssociation;
private IdentityService.Association _association;
public DeferredAuthentication(LoginAuthenticator authenticator)
public DeferredAuthenticationState(LoginAuthenticator authenticator)
{
if (authenticator == null)
throw new NullPointerException("No Authenticator");
@ -47,20 +45,27 @@ public class DeferredAuthentication implements Authentication.Deferred
}
@Override
public Authentication authenticate(Request request)
public Succeeded authenticate(Request request)
{
try
{
Authentication authentication = _authenticator.validateRequest(request, __deferredResponse, null, true);
if (authentication != null && (authentication instanceof Authentication.User) && !(authentication instanceof Authentication.ResponseSent))
AuthenticationState authenticationState = _authenticator.validateRequest(request, __deferredResponse, null);
if (authenticationState != null)
{
LoginService loginService = _authenticator.getLoginService();
IdentityService identityService = loginService.getIdentityService();
AuthenticationState.setAuthenticationState(request, authenticationState);
if (authenticationState instanceof Succeeded succeeded)
{
LoginService loginService = _authenticator.getLoginService();
IdentityService identityService = loginService.getIdentityService();
if (identityService != null)
_previousAssociation = identityService.associate(((Authentication.User)authentication).getUserIdentity());
if (identityService != null)
{
UserIdentity user = succeeded.getUserIdentity();
_association = identityService.associate(user, null);
}
return authentication;
return succeeded;
}
}
}
catch (ServerAuthException e)
@ -68,76 +73,67 @@ public class DeferredAuthentication implements Authentication.Deferred
LOG.debug("Unable to authenticate {}", request, e);
}
return this;
return null;
}
@Override
public Authentication authenticate(Request request, Response response, Callback callback)
public AuthenticationState authenticate(Request request, Response response, Callback callback)
{
try
{
LoginService loginService = _authenticator.getLoginService();
IdentityService identityService = loginService.getIdentityService();
Authentication authentication = _authenticator.validateRequest(request, response, callback, true);
if (authentication instanceof Authentication.User && identityService != null)
_previousAssociation = identityService.associate(((Authentication.User)authentication).getUserIdentity());
return authentication;
AuthenticationState authenticationState = _authenticator.validateRequest(request, response, callback);
if (authenticationState != null)
{
AuthenticationState.setAuthenticationState(request, authenticationState);
if (authenticationState instanceof Succeeded && identityService != null)
{
UserIdentity user = ((Succeeded)authenticationState).getUserIdentity();
_association = identityService.associate(user, null);
}
}
return authenticationState;
}
catch (ServerAuthException e)
{
LOG.debug("Unable to authenticate {}", request, e);
}
return this;
return null;
}
@Override
public Authentication login(String username, Object password, Request request)
public Succeeded login(String username, Object password, Request request, Response response)
{
if (username == null)
return null;
UserIdentity identity = _authenticator.login(username, password, request);
UserIdentity identity = _authenticator.login(username, password, request, response);
if (identity != null)
{
IdentityService identityService = _authenticator.getLoginService().getIdentityService();
UserAuthentication authentication = new UserAuthentication("API", identity);
AuthenticationState.Succeeded authentication = new LoginAuthenticator.UserAuthenticationSucceeded("API", identity);
if (identityService != null)
_previousAssociation = identityService.associate(identity);
_association = identityService.associate(identity, null);
return authentication;
}
return null;
}
@Override
public Authentication logout(Request request)
public void logout(Request request, Response response)
{
SecurityHandler security = SecurityHandler.getCurrentSecurityHandler();
if (security != null)
{
security.logout(null);
_authenticator.logout(request);
return new LoggedOutAuthentication(_authenticator);
}
return Authentication.UNAUTHENTICATED;
_authenticator.logout(request, response);
}
public Object getPreviousAssociation()
@Override
public IdentityService.Association getAssociation()
{
return _previousAssociation;
return _association;
}
/**
* @param response the response
* @return true if this response is from a deferred call to {@link #authenticate(Request)}
*/
public static boolean isDeferred(Response response)
{
return response == __deferredResponse;
}
private static final Response __deferredResponse = new Response()
static final Response __deferredResponse = new Deferred.DeferredResponse()
{
@Override
public Request getRequest()
@ -182,7 +178,7 @@ public class DeferredAuthentication implements Authentication.Deferred
@Override
public boolean isCommitted()
{
return false;
return true;
}
@Override

View File

@ -11,12 +11,11 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.security;
package org.eclipse.jetty.security.internal;
/**
* @version $Rev: 4701 $ $Date: 2009-03-03 13:01:26 +0100 (Tue, 03 Mar 2009) $
*/
public class RoleRunAsToken implements RunAsToken
import org.eclipse.jetty.security.IdentityService;
public class RoleRunAsToken implements IdentityService.RunAsToken
{
private final String _runAsRole;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas;
package org.eclipse.jetty.security.jaas;
import java.io.IOException;
import java.security.Principal;
@ -21,6 +21,7 @@ import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
@ -31,13 +32,13 @@ import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.eclipse.jetty.ee9.jaas.callback.DefaultCallbackHandler;
import org.eclipse.jetty.ee9.nested.UserIdentity;
import org.eclipse.jetty.ee9.security.DefaultIdentityService;
import org.eclipse.jetty.ee9.security.IdentityService;
import org.eclipse.jetty.ee9.security.LoginService;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.security.jaas.callback.DefaultCallbackHandler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
@ -45,9 +46,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* JAASLoginService
*
*
* Implementation of jetty's LoginService that works with JAAS for
* authorization and authentication.
*/
@ -55,7 +53,7 @@ public class JAASLoginService extends ContainerLifeCycle implements LoginService
{
private static final Logger LOG = LoggerFactory.getLogger(JAASLoginService.class);
public static final String DEFAULT_ROLE_CLASS_NAME = "org.eclipse.jetty.ee9.jaas.JAASRole";
public static final String DEFAULT_ROLE_CLASS_NAME = "org.eclipse.jetty.security.jaas.JAASRole";
public static final String[] DEFAULT_ROLE_CLASS_NAMES = {DEFAULT_ROLE_CLASS_NAME};
public static final ThreadLocal<JAASLoginService> INSTANCE = new ThreadLocal<>();
@ -176,17 +174,17 @@ public class JAASLoginService extends ContainerLifeCycle implements LoginService
protected void doStart() throws Exception
{
if (_identityService == null)
_identityService = new DefaultIdentityService();
_identityService = new DefaultIdentityService(); // TODO really? Should get from SecurityHandler
addBean(new PropertyUserStoreManager());
super.doStart();
}
@Override
public UserIdentity login(final String username, final Object credentials, final ServletRequest request)
public UserIdentity login(String username, Object credentials, Request request, Function<Boolean, Session> getOrCreateSession)
{
try
{
CallbackHandler callbackHandler = null;
CallbackHandler callbackHandler;
if (_callbackHandlerClass == null)
callbackHandler = new DefaultCallbackHandler();
else
@ -195,11 +193,9 @@ public class JAASLoginService extends ContainerLifeCycle implements LoginService
callbackHandler = (CallbackHandler)clazz.getDeclaredConstructor().newInstance();
}
if (callbackHandler instanceof DefaultCallbackHandler)
if (callbackHandler instanceof DefaultCallbackHandler dch)
{
DefaultCallbackHandler dch = (DefaultCallbackHandler)callbackHandler;
if (request instanceof HttpServletRequest httpServletRequest)
dch.setRequest(httpServletRequest);
dch.setRequest(request);
dch.setCredential(credentials);
dch.setUserName(username);
}
@ -281,7 +277,7 @@ public class JAASLoginService extends ContainerLifeCycle implements LoginService
groups.add(principal.getName());
}
return groups.toArray(new String[groups.size()]);
return groups.toArray(new String[0]);
}
/**

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.jaas;
package org.eclipse.jetty.security.jaas;
import java.io.Serializable;
import java.security.Principal;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas;
package org.eclipse.jetty.security.jaas;
public class JAASRole extends JAASPrincipal
{

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas;
package org.eclipse.jetty.security.jaas;
import java.security.Principal;
import javax.security.auth.Subject;

View File

@ -11,13 +11,13 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas;
package org.eclipse.jetty.security.jaas;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jetty.ee9.security.PropertyUserStore;
import org.eclipse.jetty.security.PropertyUserStore;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -40,7 +40,7 @@ public class PropertyUserStoreManager extends AbstractLifeCycle
* Map of user authentication and authorization information loaded in from a property file.
* The map is keyed off the location of the file.
*/
private Map<String, PropertyUserStore> _propertyUserStores;
private Map<String, PropertyUserStore> _propertyUserStores;
public PropertyUserStore getPropertyUserStore(String file)
{

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas.callback;
package org.eclipse.jetty.security.jaas.callback;
import java.io.IOException;
import javax.security.auth.callback.Callback;

View File

@ -11,17 +11,18 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.jaas.callback;
package org.eclipse.jetty.security.jaas.callback;
import java.io.IOException;
import java.util.Arrays;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import org.eclipse.jetty.ee10.servlet.ServletContextRequest;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.Fields;
/**
* DefaultCallbackHandler
@ -58,17 +59,18 @@ public class DefaultCallbackHandler extends AbstractCallbackHandler
}
else if (callback instanceof RequestParameterCallback)
{
ServletContextRequest servletContextRequest = Request.as(_request, ServletContextRequest.class);
if (servletContextRequest != null)
if (_request != null)
{
RequestParameterCallback rpc = (RequestParameterCallback)callback;
rpc.setParameterValues(Arrays.asList(servletContextRequest.getServletApiRequest().getParameterValues(rpc.getParameterName())));
Fields queryFields = Request.extractQueryParameters(_request);
Fields formFields = ExceptionUtil.get(FormFields.from(_request));
Fields fields = Fields.combine(queryFields, formFields);
rpc.setParameterValues(fields.getValues(rpc.getParameterName()));
}
}
else if (callback instanceof ServletRequestCallback)
else if (callback instanceof RequestCallback)
{
ServletContextRequest servletContextRequest = Request.as(_request, ServletContextRequest.class);
((ServletRequestCallback)callback).setRequest(servletContextRequest.getServletApiRequest());
((RequestCallback)callback).setRequest(_request);
}
else
throw new UnsupportedCallbackException(callback);

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.jaas.callback;
package org.eclipse.jetty.security.jaas.callback;
import javax.security.auth.callback.Callback;

View File

@ -11,27 +11,27 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas.callback;
package org.eclipse.jetty.security.jaas.callback;
import javax.security.auth.callback.Callback;
import jakarta.servlet.ServletRequest;
import org.eclipse.jetty.server.Request;
/**
* ServletRequestCallback
*
* Provides access to the request associated with the authentication.
*/
public class ServletRequestCallback implements Callback
public class RequestCallback implements Callback
{
protected ServletRequest _request;
protected Request _request;
public void setRequest(ServletRequest request)
public void setRequest(Request request)
{
_request = request;
}
public ServletRequest getRequest()
public Request getRequest()
{
return _request;
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.jaas.callback;
package org.eclipse.jetty.security.jaas.callback;
import java.util.List;
import javax.security.auth.callback.Callback;

View File

@ -14,5 +14,5 @@
/**
* Jetty Jaas : Jaas Callbacks
*/
package org.eclipse.jetty.ee9.jaas.callback;
package org.eclipse.jetty.security.jaas.callback;

View File

@ -14,5 +14,5 @@
/**
* Jetty Jaas : Support for Jaas
*/
package org.eclipse.jetty.ee10.jaas;
package org.eclipse.jetty.security.jaas;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas.spi;
package org.eclipse.jetty.security.jaas.spi;
import java.sql.Connection;
import java.sql.PreparedStatement;
@ -22,7 +22,7 @@ import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import org.eclipse.jetty.ee9.security.UserPrincipal;
import org.eclipse.jetty.security.UserPrincipal;
import org.eclipse.jetty.util.security.Credential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas.spi;
package org.eclipse.jetty.security.jaas.spi;
import java.io.IOException;
import java.util.List;
@ -27,9 +27,9 @@ import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.eclipse.jetty.ee9.jaas.JAASRole;
import org.eclipse.jetty.ee9.jaas.callback.ObjectCallback;
import org.eclipse.jetty.ee9.security.UserPrincipal;
import org.eclipse.jetty.security.UserPrincipal;
import org.eclipse.jetty.security.jaas.JAASRole;
import org.eclipse.jetty.security.jaas.callback.ObjectCallback;
/**
* AbstractLoginModule
@ -163,7 +163,7 @@ public abstract class AbstractLoginModule implements LoginModule
/**
* @return true if committed, false if not (likely not authenticated)
* @throws LoginException if unable to commit
* @see javax.security.auth.spi.LoginModule#commit()
* @see LoginModule#commit()
*/
@Override
public boolean commit() throws LoginException
@ -197,7 +197,7 @@ public abstract class AbstractLoginModule implements LoginModule
/**
* @return true if is authenticated, false otherwise
* @throws LoginException if unable to login
* @see javax.security.auth.spi.LoginModule#login()
* @see LoginModule#login()
*/
@Override
public boolean login() throws LoginException
@ -264,7 +264,7 @@ public abstract class AbstractLoginModule implements LoginModule
/**
* @return true always
* @throws LoginException if unable to logout
* @see javax.security.auth.spi.LoginModule#logout()
* @see LoginModule#logout()
*/
@Override
public boolean logout() throws LoginException
@ -279,7 +279,7 @@ public abstract class AbstractLoginModule implements LoginModule
* @param callbackHandler the callback handler
* @param sharedState the shared state map
* @param options the option map
* @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject, javax.security.auth.callback.CallbackHandler, java.util.Map, java.util.Map)
* @see LoginModule#initialize(Subject, CallbackHandler, Map, Map)
*/
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler,

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas.spi;
package org.eclipse.jetty.security.jaas.spi;
import java.sql.Connection;
import java.util.Map;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.jaas.spi;
package org.eclipse.jetty.security.jaas.spi;
import java.sql.Connection;
import java.sql.DriverManager;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas.spi;
package org.eclipse.jetty.security.jaas.spi;
import java.io.IOException;
import java.util.ArrayList;
@ -39,8 +39,8 @@ import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import org.eclipse.jetty.ee9.jaas.callback.ObjectCallback;
import org.eclipse.jetty.ee9.security.UserPrincipal;
import org.eclipse.jetty.security.UserPrincipal;
import org.eclipse.jetty.security.jaas.callback.ObjectCallback;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.security.Credential;
import org.slf4j.Logger;
@ -532,7 +532,7 @@ public class LdapLoginModule extends AbstractLoginModule
setAuthenticated(true);
return true;
}
catch (javax.naming.AuthenticationException e)
catch (AuthenticationException e)
{
throw new FailedLoginException(e.getMessage());
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.ee9.jaas.spi;
package org.eclipse.jetty.security.jaas.spi;
import java.util.Collections;
import java.util.List;
@ -20,11 +20,11 @@ import java.util.stream.Collectors;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import org.eclipse.jetty.ee9.jaas.JAASLoginService;
import org.eclipse.jetty.ee9.jaas.PropertyUserStoreManager;
import org.eclipse.jetty.ee9.security.PropertyUserStore;
import org.eclipse.jetty.ee9.security.RolePrincipal;
import org.eclipse.jetty.ee9.security.UserPrincipal;
import org.eclipse.jetty.security.PropertyUserStore;
import org.eclipse.jetty.security.RolePrincipal;
import org.eclipse.jetty.security.UserPrincipal;
import org.eclipse.jetty.security.jaas.JAASLoginService;
import org.eclipse.jetty.security.jaas.PropertyUserStoreManager;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.slf4j.Logger;
@ -48,8 +48,8 @@ public class PropertyFileLoginModule extends AbstractLoginModule
* @param callbackHandler the callback handler
* @param sharedState the shared state map
* @param options the options map
* @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject, javax.security.auth.callback.CallbackHandler, java.util.Map,
* java.util.Map)
* @see javax.security.auth.spi.LoginModule#initialize(Subject, CallbackHandler, Map,
* Map)
*/
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options)
@ -77,17 +77,17 @@ public class PropertyFileLoginModule extends AbstractLoginModule
_store = mgr.getPropertyUserStore(filename);
if (_store == null)
{
int refreshInterval = 0;
String tmp = (String)options.get("refreshInterval");
int reloadInterval = 0;
String tmp = (String)options.get("reloadInterval");
if (tmp != null)
{
try
{
refreshInterval = Integer.parseInt(tmp);
reloadInterval = Integer.parseInt(tmp);
}
catch (NumberFormatException e)
{
LOG.warn("'refreshInterval' is not an integer");
LOG.warn("'reloadInterval' is not an integer");
}
}
else
@ -95,15 +95,15 @@ public class PropertyFileLoginModule extends AbstractLoginModule
tmp = (String)options.get("hotReload");
if (tmp != null)
{
LOG.warn("Use 'refreshInterval' boolean property instead of 'hotReload'");
refreshInterval = Boolean.parseBoolean(tmp) ? 1 : 0;
LOG.warn("Use 'reloadInterval' boolean property instead of 'hotReload'");
reloadInterval = Boolean.parseBoolean(tmp) ? 1 : 0;
}
}
PropertyUserStore newStore = new PropertyUserStore();
ResourceFactory resourceFactory = ResourceFactory.of(newStore);
Resource config = resourceFactory.newResource(filename);
newStore.setConfig(config);
newStore.setRefreshInterval(refreshInterval);
newStore.setReloadInterval(reloadInterval);
_store = mgr.addPropertyUserStore(filename, newStore);
try
{

View File

@ -14,5 +14,5 @@
/**
* Jetty Jaas : Various Jaas Implementations for Jetty
*/
package org.eclipse.jetty.ee9.jaas.spi;
package org.eclipse.jetty.security.jaas.spi;

View File

@ -14,5 +14,5 @@
/**
* Jetty Security : Modular Support for Security in Jetty
*/
package org.eclipse.jetty.ee10.servlet.security;
package org.eclipse.jetty.security;

View File

@ -11,8 +11,9 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.jaas;
package org.eclipse.jetty.security.jaas;
/* TODO
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
@ -50,9 +51,6 @@ import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;
/**
* JAASLdapLoginServiceTest
*/
@RunWith(FrameworkRunner.class)
@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP")})
@CreateDS(allowAnonAccess = false, partitions = {
@ -125,8 +123,11 @@ import static org.hamcrest.Matchers.startsWith;
"uniquemember: uid=uniqueuser,ou=subdir,ou=people,dc=jetty,dc=org",
"cn: admin"
})
*/
public class JAASLdapLoginServiceTest
{
/* TODO restore this test
public static class TestConfiguration extends Configuration
{
private boolean forceBindingLogin;
@ -311,4 +312,6 @@ public class JAASLdapLoginServiceTest
String response = doLogin("ambiguousone", "barfoo", null, null);
assertThat(response, startsWith("HTTP/1.1 " + HttpServletResponse.SC_UNAUTHORIZED));
}
*/
}

View File

@ -11,30 +11,29 @@
// ========================================================================
//
package org.eclipse.jetty.ee10.jaas;
package org.eclipse.jetty.security.jaas;
import java.io.IOException;
import java.security.Principal;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
import javax.security.auth.login.Configuration;
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.security.ConstraintMapping;
import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
import org.eclipse.jetty.ee10.servlet.security.DefaultIdentityService;
import org.eclipse.jetty.ee10.servlet.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.Handler;
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.util.security.Constraint;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -94,22 +93,24 @@ public class JAASLoginServiceTest
return _name;
}
}
public static class TestServlet extends HttpServlet
public static class TestHandler extends Handler.Abstract
{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
resp.setStatus(200);
resp.setContentType("text/plain");
resp.getWriter().println("All OK");
resp.getWriter().println("requestURI=" + req.getRequestURI());
response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/plain");
Content.Sink.write(response, true, """
All OK
httpURI=%s
""".formatted(request.getHttpURI()), callback);
return true;
}
}
private Server _server;
private LocalConnector _connector;
private ServletContextHandler _context;
private ContextHandler _context;
@BeforeEach
public void setUp() throws Exception
@ -118,19 +119,14 @@ public class JAASLoginServiceTest
_connector = new LocalConnector(_server);
_server.addConnector(_connector);
_context = new ServletContextHandler();
_context.setContextPath("/ctx");
_context.addServlet(new TestServlet(), "/");
_context = new ContextHandler("/ctx");
_server.setHandler(_context);
ConstraintSecurityHandler security = new ConstraintSecurityHandler();
SecurityHandler.PathMapped security = new SecurityHandler.PathMapped();
security.setAuthenticator(new BasicAuthenticator());
Constraint constraint = new Constraint("All", "users");
constraint.setAuthenticate(true);
ConstraintMapping mapping = new ConstraintMapping();
mapping.setPathSpec("/jaspi/*");
mapping.setConstraint(constraint);
security.addConstraintMapping(mapping);
_context.setSecurityHandler(security);
Constraint constraint = Constraint.ANY_USER;
security.put("/jaspi/*", constraint);
_context.setHandler(security);
security.setHandler(new TestHandler());
}
@AfterEach
@ -149,7 +145,7 @@ public class JAASLoginServiceTest
{
return new AppConfigurationEntry[] {
new AppConfigurationEntry(TestLoginModule.class.getCanonicalName(),
LoginModuleControlFlag.REQUIRED,
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
Collections.emptyMap())
};
}
@ -157,7 +153,7 @@ public class JAASLoginServiceTest
//Test with the DefaultCallbackHandler
JAASLoginService ls = new JAASLoginService("foo");
ls.setCallbackHandlerClass("org.eclipse.jetty.ee10.jaas.callback.DefaultCallbackHandler");
ls.setCallbackHandlerClass("org.eclipse.jetty.security.jaas.callback.DefaultCallbackHandler");
ls.setIdentityService(new DefaultIdentityService());
ls.setConfiguration(config);
_server.addBean(ls, true);

View File

@ -0,0 +1,101 @@
//
// ========================================================================
// 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.jaas;
import java.util.List;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
public class TestHandler extends Handler.Abstract
{
private List<String> _hasRoles;
private List<String> _hasntRoles;
public TestHandler(List<String> hasRoles, List<String> hasntRoles)
{
_hasRoles = hasRoles;
_hasntRoles = hasntRoles;
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
if (_hasRoles == null && _hasntRoles == null)
{
try
{
// TODO this is wrong
AuthenticationState.authenticate(request, response, callback);
}
catch (Throwable e)
{
//TODO: an Exception should only be thrown here if the response has
//not been set, but currently it seems that the ServletException is thrown
//anyway by ServletContextRequest.ServletApiRequest.authenticate.
}
}
else
{
testHasRoles(request, response);
testHasntRoles(request, response);
response.setStatus(200);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
Content.Sink.write(response, true, "All OK\nrequestURI=" + request.getHttpURI(), callback);
}
return true;
}
private void testHasRoles(Request request, Response response)
{
if (_hasRoles != null)
{
AuthenticationState authenticationState = AuthenticationState.getAuthenticationState(request);
if (authenticationState instanceof AuthenticationState.Succeeded userAuthentication)
{
for (String role : _hasRoles)
{
if (!userAuthentication.isUserInRole(role))
throw new BadMessageException(HttpStatus.FORBIDDEN_403, "! in role " + role);
}
}
}
}
private void testHasntRoles(Request request, Response response)
{
if (_hasntRoles != null)
{
AuthenticationState authenticationState = AuthenticationState.getAuthenticationState(request);
if (authenticationState instanceof AuthenticationState.Succeeded userAuthentication)
{
for (String role : _hasntRoles)
{
if (userAuthentication.isUserInRole(role))
throw new HttpException.RuntimeException(500, "in role " + role);
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More