Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring back some openid improvements from 10.0.x to 9.4.x #9660

Merged
merged 2 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,7 @@ public Authentication validateRequest(ServletRequest req, ServletResponse res, b
{
if (LOG.isDebugEnabled())
LOG.debug("auth revoked {}", authentication);
synchronized (session)
{
session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
}
logout(request);
}
else
{
Expand Down Expand Up @@ -380,10 +377,11 @@ public Authentication validateRequest(ServletRequest req, ServletResponse res, b
}
}
}

if (LOG.isDebugEnabled())
LOG.debug("auth {}", authentication);
return authentication;
}
if (LOG.isDebugEnabled())
LOG.debug("auth {}", authentication);
return authentication;
}

// If we can't send challenge.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.io.Serializable;
import java.net.URI;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -142,12 +143,24 @@ private void validateClaims(OpenIdConfiguration configuration) throws Exception
throw new AuthenticationException("Authorized party claim value should be the client_id");

// Check that the ID token has not expired by checking the exp claim.
long expiry = (Long)claims.get("exp");
long currentTimeSeconds = (long)(System.currentTimeMillis() / 1000F);
if (currentTimeSeconds > expiry)
if (isExpired())
throw new AuthenticationException("ID Token has expired");
}

public boolean isExpired()
{
return checkExpiry(claims);
}

public static boolean checkExpiry(Map<String, Object> claims)
{
if (claims == null)
return true;

// Check that the ID token has not expired by checking the exp claim.
return Instant.ofEpochSecond((Long)claims.get("exp")).isBefore(Instant.now());
}

private void validateAudience(OpenIdConfiguration configuration) throws AuthenticationException
{
Object aud = claims.get("aud");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,41 @@

package org.eclipse.jetty.security.openid;

import java.io.File;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.AbstractLoginService;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.session.FileSessionDataStoreFactory;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
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.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;

public class OpenIdAuthenticationTest
Expand All @@ -52,8 +65,12 @@ public class OpenIdAuthenticationTest
private ServerConnector connector;
private HttpClient client;

@BeforeEach
public void setup() throws Exception
public void setup(LoginService loginService) throws Exception
{
setup(loginService, null);
}

public void setup(LoginService loginService, Consumer<OpenIdConfiguration> configure) throws Exception
{
openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET);
openIdProvider.start();
Expand Down Expand Up @@ -93,22 +110,29 @@ public void setup() throws Exception

// security handler
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.setRealmName("OpenID Connect Authentication");
assertThat(securityHandler.getKnownAuthenticatorFactories().size(), greaterThanOrEqualTo(2));

securityHandler.setAuthMethod(Constraint.__OPENID_AUTH);
securityHandler.setRealmName(openIdProvider.getProvider());
securityHandler.addConstraintMapping(profileMapping);
securityHandler.addConstraintMapping(loginMapping);
securityHandler.addConstraintMapping(adminMapping);

// Authentication using local OIDC Provider
OpenIdConfiguration configuration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);

// Configure OpenIdLoginService optionally providing a base LoginService to provide user roles
OpenIdLoginService loginService = new OpenIdLoginService(configuration);
securityHandler.setLoginService(loginService);

Authenticator authenticator = new OpenIdAuthenticator(configuration, "/error");
securityHandler.setAuthenticator(authenticator);
OpenIdConfiguration openIdConfiguration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);
if (configure != null)
configure.accept(openIdConfiguration);
securityHandler.setLoginService(new OpenIdLoginService(openIdConfiguration, loginService));
server.addBean(openIdConfiguration);
securityHandler.setInitParameter(OpenIdAuthenticator.ERROR_PAGE, "/error");
context.setSecurityHandler(securityHandler);

File datastoreDir = MavenTestingUtils.getTargetTestingDir("datastore");
IO.delete(datastoreDir);
FileSessionDataStoreFactory fileSessionDataStoreFactory = new FileSessionDataStoreFactory();
fileSessionDataStoreFactory.setStoreDir(datastoreDir);
server.addBean(fileSessionDataStoreFactory);

server.start();
String redirectUri = "http://localhost:" + connector.getLocalPort() + "/j_security_check";
openIdProvider.addRedirectUri(redirectUri);
Expand All @@ -127,58 +151,146 @@ public void stop() throws Exception
@Test
public void testLoginLogout() throws Exception
{
setup(null);
openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));

String appUriString = "http://localhost:" + connector.getLocalPort();

// Initially not authenticated
ContentResponse response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String[] content = response.getContentAsString().split("[\r\n]+");
assertThat(content.length, is(1));
assertThat(content[0], is("not authenticated"));
String content = response.getContentAsString();
assertThat(content, containsString("not authenticated"));

// Request to login is success
response = client.GET(appUriString + "/login");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("[\r\n]+");
assertThat(content.length, is(1));
assertThat(content[0], is("success"));
content = response.getContentAsString();
assertThat(content, containsString("success"));

// Now authenticated we can get info
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("[\r\n]+");
assertThat(content.length, is(3));
assertThat(content[0], is("userId: 123456789"));
assertThat(content[1], is("name: Alice"));
assertThat(content[2], is("email: [email protected]"));
content = response.getContentAsString();
assertThat(content, containsString("userId: 123456789"));
assertThat(content, containsString("name: Alice"));
assertThat(content, containsString("email: [email protected]"));

// Request to admin page gives 403 as we do not have admin role
response = client.GET(appUriString + "/admin");
assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN_403));

// We can restart the server and still be logged in as we have persistent session datastore.
server.stop();
server.start();
appUriString = "http://localhost:" + connector.getLocalPort();

// After restarting server the authentication is saved as a session authentication.
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("userId: 123456789"));
assertThat(content, containsString("name: Alice"));
assertThat(content, containsString("email: [email protected]"));

// We are no longer authenticated after logging out
response = client.GET(appUriString + "/logout");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("[\r\n]+");
assertThat(content.length, is(1));
assertThat(content[0], is("not authenticated"));
content = response.getContentAsString();
assertThat(content, containsString("not authenticated"));

// Test that the user was logged out successfully on the openid provider.
assertThat(openIdProvider.getLoggedInUsers().getMax(), equalTo(1L));
assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
}

@Test
public void testNestedLoginService() throws Exception
{
AtomicBoolean loggedIn = new AtomicBoolean(true);
setup(new AbstractLoginService()
{
@Override
protected String[] loadRoleInfo(UserPrincipal user)
{
return new String[]{"admin"};
}

@Override
protected UserPrincipal loadUserInfo(String username)
{
return new UserPrincipal(username, new Password(""));
}

@Override
public boolean validate(UserIdentity user)
{
if (!loggedIn.get())
return false;
return super.validate(user);
}
});

openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));

String appUriString = "http://localhost:" + connector.getLocalPort();

// Initially not authenticated
ContentResponse response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String content = response.getContentAsString();
assertThat(content, containsString("not authenticated"));

// Request to login is success
response = client.GET(appUriString + "/login");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("success"));

// Now authenticated we can get info
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString();
assertThat(content, containsString("userId: 123456789"));
assertThat(content, containsString("name: Alice"));
assertThat(content, containsString("email: [email protected]"));

// The nested login service has supplied the admin role.
response = client.GET(appUriString + "/admin");
assertThat(response.getStatus(), is(HttpStatus.OK_200));

// This causes any validation of UserIdentity in the LoginService to fail
// causing subsequent requests to be redirected to the auth endpoint for login again.
loggedIn.set(false);
client.setFollowRedirects(false);
response = client.GET(appUriString + "/admin");
assertThat(response.getStatus(), is(HttpStatus.SEE_OTHER_303));
String location = response.getHeaders().get(HttpHeader.LOCATION);
assertThat(location, containsString(openIdProvider.getProvider() + "/auth"));

// Note that we couldn't follow "OpenID Connect RP-Initiated Logout 1.0" because we redirect straight to auth endpoint.
assertThat(openIdProvider.getLoggedInUsers().getCurrent(), equalTo(1L));
assertThat(openIdProvider.getLoggedInUsers().getMax(), equalTo(1L));
assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
}

public static class LoginPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/html");
response.getWriter().println("success");
response.getWriter().println("<br><a href=\"/\">Home</a>");
}
}

public static class LogoutPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
request.getSession().invalidate();
request.logout();
response.sendRedirect("/");
}
}
Expand All @@ -188,7 +300,7 @@ public static class AdminPage extends HttpServlet
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
Map<String, Object> userInfo = (Map<String, Object>)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println(userInfo.get("sub") + ": success");
}
}
Expand All @@ -198,18 +310,20 @@ public static class HomePage extends HttpServlet
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/plain");
response.setContentType("text/html");
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null)
{
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println("userId: " + userInfo.get("sub"));
response.getWriter().println("name: " + userInfo.get("name"));
response.getWriter().println("email: " + userInfo.get("email"));
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>");
}
else
{
response.getWriter().println("not authenticated");
response.getWriter().println("<br><a href=\"/login\">Login</a>");
}
}
}
Expand All @@ -219,8 +333,9 @@ public static class ErrorPage extends HttpServlet
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/plain");
response.setContentType("text/html");
response.getWriter().println("not authorized");
response.getWriter().println("<br><a href=\"/\">Home</a>");
}
}
}
Loading