Issue #9464 - Add optional configuration to log user out after OpenID idToken expires. (Jetty-10) (#9528)
* improvements to logout from the OpenIdLoginService validate * respect idToken expiry for lifetime of login * fix checkstyle error * Add respectIdTokenExpiry configuration * changes from review * rename respectIdTokenExpiry to logoutWhenIdTokenIsExpired * changes from review --------- Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
parent
81efae2f98
commit
24b7d06fd5
|
@ -38,6 +38,9 @@
|
|||
<Set name="authenticateNewUsers">
|
||||
<Property name="jetty.openid.authenticateNewUsers" default="false"/>
|
||||
</Set>
|
||||
<Set name="logoutWhenIdTokenIsExpired">
|
||||
<Property name="jetty.openid.logoutWhenIdTokenIsExpired" default="false"/>
|
||||
</Set>
|
||||
<Call name="addScopes">
|
||||
<Arg>
|
||||
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
|
||||
|
|
|
@ -45,3 +45,6 @@ etc/jetty-openid.xml
|
|||
|
||||
## What authentication method to use with the Token Endpoint (client_secret_post, client_secret_basic).
|
||||
# jetty.openid.authMethod=client_secret_post
|
||||
|
||||
## Whether the user should be logged out after the idToken expires.
|
||||
# jetty.openid.logoutWhenIdTokenIsExpired=false
|
||||
|
|
|
@ -248,6 +248,11 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
public void logout(ServletRequest request)
|
||||
{
|
||||
attemptLogoutRedirect(request);
|
||||
logoutWithoutRedirect(request);
|
||||
}
|
||||
|
||||
private void logoutWithoutRedirect(ServletRequest request)
|
||||
{
|
||||
super.logout(request);
|
||||
HttpServletRequest httpRequest = (HttpServletRequest)request;
|
||||
HttpSession session = httpRequest.getSession(false);
|
||||
|
@ -265,13 +270,13 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
}
|
||||
|
||||
/**
|
||||
* This will attempt to redirect the request to the end_session_endpoint, and finally to the {@link #REDIRECT_PATH}.
|
||||
* <p>This will attempt to redirect the request to the end_session_endpoint, and finally to the {@link #REDIRECT_PATH}.</p>
|
||||
*
|
||||
* If end_session_endpoint is defined the request will be redirected to the end_session_endpoint, the optional
|
||||
* post_logout_redirect_uri parameter will be set if {@link #REDIRECT_PATH} is non-null.
|
||||
* <p>If end_session_endpoint is defined the request will be redirected to the end_session_endpoint, the optional
|
||||
* post_logout_redirect_uri parameter will be set if {@link #REDIRECT_PATH} is non-null.</p>
|
||||
*
|
||||
* If the end_session_endpoint is not defined then the request will be redirected to {@link #REDIRECT_PATH} if it is a
|
||||
* non-null value, otherwise no redirection will be done.
|
||||
* <p>If the end_session_endpoint is not defined then the request will be redirected to {@link #REDIRECT_PATH} if it is a
|
||||
* non-null value, otherwise no redirection will be done.</p>
|
||||
*
|
||||
* @param request the request to redirect.
|
||||
*/
|
||||
|
@ -366,6 +371,17 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
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(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
|
||||
{
|
||||
|
@ -381,6 +397,17 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
if (uri == null)
|
||||
uri = URIUtil.SLASH;
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
if (_openIdConfiguration.isLogoutWhenIdTokenIsExpired() && hasExpiredIdToken(session))
|
||||
{
|
||||
// After logout, fall through to the code below and send another login challenge.
|
||||
logoutWithoutRedirect(request);
|
||||
|
||||
// If we expired a valid authentication we do not want to defer authentication,
|
||||
// we want to try re-authenticate the user.
|
||||
mandatory = true;
|
||||
}
|
||||
|
||||
mandatory |= isJSecurityCheck(uri);
|
||||
if (!mandatory)
|
||||
return new DeferredAuthentication(this);
|
||||
|
@ -391,7 +418,9 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
try
|
||||
{
|
||||
// Get the Session.
|
||||
HttpSession session = request.getSession();
|
||||
if (session == null)
|
||||
session = request.getSession(true);
|
||||
|
||||
if (request.isRequestedSessionIdFromURL())
|
||||
{
|
||||
sendError(request, response, "Session ID must be a cookie to support OpenID authentication");
|
||||
|
@ -464,10 +493,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("auth revoked {}", authentication);
|
||||
synchronized (session)
|
||||
{
|
||||
session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
|
||||
}
|
||||
logoutWithoutRedirect(request);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -499,10 +525,10 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
}
|
||||
}
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("auth {}", authentication);
|
||||
return authentication;
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("auth {}", authentication);
|
||||
return authentication;
|
||||
}
|
||||
|
||||
// If we can't send challenge.
|
||||
|
@ -513,12 +539,11 @@ public class OpenIdAuthenticator extends LoginAuthenticator
|
|||
return Authentication.UNAUTHENTICATED;
|
||||
}
|
||||
|
||||
// Send the the challenge.
|
||||
// Send the challenge.
|
||||
String challengeUri = getChallengeUri(baseRequest);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("challenge {}->{}", session.getId(), challengeUri);
|
||||
baseResponse.sendRedirect(challengeUri, true);
|
||||
|
||||
return Authentication.SEND_CONTINUE;
|
||||
}
|
||||
catch (IOException e)
|
||||
|
|
|
@ -55,6 +55,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
|
|||
private String tokenEndpoint;
|
||||
private String endSessionEndpoint;
|
||||
private boolean authenticateNewUsers = false;
|
||||
private boolean logoutWhenIdTokenIsExpired = false;
|
||||
|
||||
/**
|
||||
* Create an OpenID configuration for a specific OIDC provider.
|
||||
|
@ -275,6 +276,16 @@ public class OpenIdConfiguration extends ContainerLifeCycle
|
|||
this.authenticateNewUsers = authenticateNewUsers;
|
||||
}
|
||||
|
||||
public boolean isLogoutWhenIdTokenIsExpired()
|
||||
{
|
||||
return logoutWhenIdTokenIsExpired;
|
||||
}
|
||||
|
||||
public void setLogoutWhenIdTokenIsExpired(boolean logoutWhenIdTokenIsExpired)
|
||||
{
|
||||
this.logoutWhenIdTokenIsExpired = logoutWhenIdTokenIsExpired;
|
||||
}
|
||||
|
||||
private static HttpClient newHttpClient()
|
||||
{
|
||||
ClientConnector connector = new ClientConnector();
|
||||
|
|
|
@ -15,6 +15,7 @@ package org.eclipse.jetty.security.openid;
|
|||
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -137,12 +138,24 @@ public class OpenIdCredentials implements Serializable
|
|||
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");
|
||||
|
|
|
@ -136,7 +136,9 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
|
|||
{
|
||||
if (!(user.getUserPrincipal() instanceof OpenIdUserPrincipal))
|
||||
return false;
|
||||
|
||||
OpenIdUserPrincipal userPrincipal = (OpenIdUserPrincipal)user.getUserPrincipal();
|
||||
if (configuration.isLogoutWhenIdTokenIsExpired() && userPrincipal.getCredentials().isExpired())
|
||||
return false;
|
||||
return loginService == null || loginService.validate(user);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,10 @@ package org.eclipse.jetty.security.openid;
|
|||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
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;
|
||||
|
@ -24,18 +27,24 @@ 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.AbstractLoginService;
|
||||
import org.eclipse.jetty.security.ConstraintMapping;
|
||||
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
||||
import org.eclipse.jetty.security.LoginService;
|
||||
import org.eclipse.jetty.security.RolePrincipal;
|
||||
import org.eclipse.jetty.security.UserPrincipal;
|
||||
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;
|
||||
|
@ -43,6 +52,7 @@ import static org.hamcrest.Matchers.containsString;
|
|||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class OpenIdAuthenticationTest
|
||||
|
@ -55,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();
|
||||
|
@ -100,12 +114,16 @@ public class OpenIdAuthenticationTest
|
|||
|
||||
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
|
||||
server.addBean(new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET));
|
||||
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, "/");
|
||||
|
@ -135,6 +153,7 @@ public class OpenIdAuthenticationTest
|
|||
@Test
|
||||
public void testLoginLogout() throws Exception
|
||||
{
|
||||
setup(null);
|
||||
openIdProvider.setUser(new OpenIdProvider.User("123456789", "Alice"));
|
||||
|
||||
String appUriString = "http://localhost:" + connector.getLocalPort();
|
||||
|
@ -188,6 +207,163 @@ public class OpenIdAuthenticationTest
|
|||
assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedLoginService() throws Exception
|
||||
{
|
||||
AtomicBoolean loggedIn = new AtomicBoolean(true);
|
||||
setup(new AbstractLoginService()
|
||||
{
|
||||
|
||||
@Override
|
||||
protected List<RolePrincipal> loadRoleInfo(UserPrincipal user)
|
||||
{
|
||||
return List.of(new RolePrincipal("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: Alice@example.com"));
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpiredIdToken() throws Exception
|
||||
{
|
||||
setup(null, config -> config.setLogoutWhenIdTokenIsExpired(true));
|
||||
long idTokenExpiryTime = 2000;
|
||||
openIdProvider.setIdTokenDuration(idTokenExpiryTime);
|
||||
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
|
||||
client.setFollowRedirects(false);
|
||||
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: Alice@example.com"));
|
||||
|
||||
// After waiting past ID_Token expiry time we are no longer authenticated.
|
||||
// Even though this page is non-mandatory authentication the OpenId attributes should be cleared.
|
||||
// This then attempts re-authorization the first time even though it is non-mandatory page.
|
||||
Thread.sleep(idTokenExpiryTime * 2);
|
||||
response = client.GET(appUriString + "/");
|
||||
assertThat(response.getStatus(), is(HttpStatus.SEE_OTHER_303));
|
||||
assertThat(response.getHeaders().get(HttpHeader.LOCATION), startsWith(openIdProvider.getProvider() + "/auth"));
|
||||
|
||||
// User was never redirected to logout page.
|
||||
assertThat(openIdProvider.getLoggedInUsers().getCurrent(), equalTo(1L));
|
||||
assertThat(openIdProvider.getLoggedInUsers().getMax(), equalTo(1L));
|
||||
assertThat(openIdProvider.getLoggedInUsers().getTotal(), equalTo(1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpiredIdTokenDisabled() throws Exception
|
||||
{
|
||||
setup(null);
|
||||
long idTokenExpiryTime = 2000;
|
||||
openIdProvider.setIdTokenDuration(idTokenExpiryTime);
|
||||
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
|
||||
client.setFollowRedirects(false);
|
||||
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: Alice@example.com"));
|
||||
|
||||
// After waiting past ID_Token expiry time we are still authenticated because logoutWhenIdTokenIsExpired is false by default.
|
||||
Thread.sleep(idTokenExpiryTime * 2);
|
||||
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: Alice@example.com"));
|
||||
}
|
||||
|
||||
public static class LoginPage extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
|
|
|
@ -16,6 +16,7 @@ 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;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
@ -36,7 +37,6 @@ import org.eclipse.jetty.server.Server;
|
|||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.statistic.CounterStatistic;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -61,13 +61,14 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
private String provider;
|
||||
private User preAuthedUser;
|
||||
private final CounterStatistic loggedInUsers = new CounterStatistic();
|
||||
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
String clientId = "CLIENT_ID123";
|
||||
String clientSecret = "PASSWORD123";
|
||||
int port = 5771;
|
||||
String redirectUri = "http://localhost:8080/openid/auth";
|
||||
String redirectUri = "http://localhost:8080/j_security_check";
|
||||
|
||||
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
|
||||
openIdProvider.addRedirectUri(redirectUri);
|
||||
|
@ -103,6 +104,16 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
addBean(server);
|
||||
}
|
||||
|
||||
public void setIdTokenDuration(long duration)
|
||||
{
|
||||
_idTokenDuration = duration;
|
||||
}
|
||||
|
||||
public long getIdTokenDuration()
|
||||
{
|
||||
return _idTokenDuration;
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException
|
||||
{
|
||||
server.join();
|
||||
|
@ -173,7 +184,7 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
}
|
||||
|
||||
String scopeString = req.getParameter("scope");
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
|
||||
if (!scopes.contains("openid"))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
|
||||
|
@ -286,11 +297,11 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
}
|
||||
|
||||
String accessToken = "ABCDEFG";
|
||||
long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();
|
||||
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
|
||||
String response = "{" +
|
||||
"\"access_token\": \"" + accessToken + "\"," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId)) + "\"," +
|
||||
"\"expires_in\": " + expiry + "," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
|
||||
"\"expires_in\": " + accessTokenDuration + "," +
|
||||
"\"token_type\": \"Bearer\"" +
|
||||
"}";
|
||||
|
||||
|
@ -374,10 +385,10 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
return subject;
|
||||
}
|
||||
|
||||
public String getIdToken(String provider, String clientId)
|
||||
public String getIdToken(String provider, String clientId, long duration)
|
||||
{
|
||||
long expiry = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiry);
|
||||
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -387,5 +398,11 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
return false;
|
||||
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(subject, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ package org.eclipse.jetty.tests.distribution.openid;
|
|||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
@ -37,8 +38,8 @@ import org.eclipse.jetty.server.Server;
|
|||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.statistic.CounterStatistic;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -49,6 +50,7 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
|
||||
private static final String AUTH_PATH = "/auth";
|
||||
private static final String TOKEN_PATH = "/token";
|
||||
private static final String END_SESSION_PATH = "/end_session";
|
||||
private final Map<String, User> issuedAuthCodes = new HashMap<>();
|
||||
|
||||
protected final String clientId;
|
||||
|
@ -59,13 +61,15 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
private int port = 0;
|
||||
private String provider;
|
||||
private User preAuthedUser;
|
||||
private final CounterStatistic loggedInUsers = new CounterStatistic();
|
||||
private long _idTokenDuration = Duration.ofSeconds(10).toMillis();
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
String clientId = "CLIENT_ID123";
|
||||
String clientSecret = "PASSWORD123";
|
||||
int port = 5771;
|
||||
String redirectUri = "http://localhost:8080/openid/auth";
|
||||
String redirectUri = "http://localhost:8080/j_security_check";
|
||||
|
||||
OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret);
|
||||
openIdProvider.addRedirectUri(redirectUri);
|
||||
|
@ -92,14 +96,25 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
|
||||
ServletContextHandler contextHandler = new ServletContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
contextHandler.addServlet(new ServletHolder(new OpenIdConfigServlet()), CONFIG_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new OpenIdAuthEndpoint()), AUTH_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new OpenIdTokenEndpoint()), TOKEN_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH);
|
||||
contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH);
|
||||
server.setHandler(contextHandler);
|
||||
|
||||
addBean(server);
|
||||
}
|
||||
|
||||
public void setIdTokenDuration(long duration)
|
||||
{
|
||||
_idTokenDuration = duration;
|
||||
}
|
||||
|
||||
public long getIdTokenDuration()
|
||||
{
|
||||
return _idTokenDuration;
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException
|
||||
{
|
||||
server.join();
|
||||
|
@ -113,6 +128,11 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null);
|
||||
}
|
||||
|
||||
public CounterStatistic getLoggedInUsers()
|
||||
{
|
||||
return loggedInUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
|
@ -145,7 +165,7 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
redirectUris.add(uri);
|
||||
}
|
||||
|
||||
public class OpenIdAuthEndpoint extends HttpServlet
|
||||
public class AuthEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
|
@ -165,7 +185,7 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
}
|
||||
|
||||
String scopeString = req.getParameter("scope");
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
|
||||
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" "));
|
||||
if (!scopes.contains("openid"))
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
|
||||
|
@ -253,7 +273,7 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
}
|
||||
}
|
||||
|
||||
public class OpenIdTokenEndpoint extends HttpServlet
|
||||
private class TokenEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
|
@ -278,20 +298,53 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
}
|
||||
|
||||
String accessToken = "ABCDEFG";
|
||||
long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();
|
||||
long accessTokenDuration = Duration.ofMinutes(10).toSeconds();
|
||||
String response = "{" +
|
||||
"\"access_token\": \"" + accessToken + "\"," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId)) + "\"," +
|
||||
"\"expires_in\": " + expiry + "," +
|
||||
"\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," +
|
||||
"\"expires_in\": " + accessTokenDuration + "," +
|
||||
"\"token_type\": \"Bearer\"" +
|
||||
"}";
|
||||
|
||||
loggedInUsers.increment();
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().print(response);
|
||||
}
|
||||
}
|
||||
|
||||
public class OpenIdConfigServlet extends HttpServlet
|
||||
private class EndSessionEndpoint extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
doPost(req, resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
{
|
||||
String idToken = req.getParameter("id_token_hint");
|
||||
if (idToken == null)
|
||||
{
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint");
|
||||
return;
|
||||
}
|
||||
|
||||
String logoutRedirect = req.getParameter("post_logout_redirect_uri");
|
||||
if (logoutRedirect == null)
|
||||
{
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
resp.getWriter().println("logout success on end_session_endpoint");
|
||||
return;
|
||||
}
|
||||
|
||||
loggedInUsers.decrement();
|
||||
resp.setContentType("text/plain");
|
||||
resp.sendRedirect(logoutRedirect);
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
|
||||
|
@ -300,6 +353,7 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
"\"issuer\": \"" + provider + "\"," +
|
||||
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
|
||||
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
|
||||
"\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," +
|
||||
"}";
|
||||
|
||||
resp.getWriter().write(discoveryDocument);
|
||||
|
@ -332,10 +386,24 @@ public class OpenIdProvider extends ContainerLifeCycle
|
|||
return subject;
|
||||
}
|
||||
|
||||
public String getIdToken(String provider, String clientId)
|
||||
public String getIdToken(String provider, String clientId, long duration)
|
||||
{
|
||||
long expiry = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiry);
|
||||
long expiryTime = Instant.now().plusMillis(duration).getEpochSecond();
|
||||
return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (!(obj instanceof User))
|
||||
return false;
|
||||
return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(subject, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue