saml backchannel logout
This commit is contained in:
parent
c609ae3496
commit
6d5ab0f66b
12 changed files with 615 additions and 512 deletions
|
@ -68,12 +68,12 @@
|
|||
<span tooltip-placement="right" tooltip="Valid URI pattern a browser can redirect to after a successful login or logout. Simple wildcards are allowed i.e. 'http://example.com/*'. Relative path can be specified too i.e. /my/relative/path/*. Relative paths will generate a redirect URI using the request's host and port." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="!application.bearerOnly && !create">
|
||||
<label class="col-sm-2 control-label" for="baseUrl">Base URL</label>
|
||||
<label class="col-sm-2 control-label" for="baseUrl">Default Redirect URL</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="baseUrl" id="baseUrl"
|
||||
data-ng-model="application.baseUrl">
|
||||
</div>
|
||||
<span tooltip-placement="right" tooltip="Optional URL to use when linking to this application. i.e. the welcome page." class="fa fa-info-circle"></span>
|
||||
<span tooltip-placement="right" tooltip="Default URL to use when no redirect URI is specified. This URL will also be used when the auth server needs to link to the application for any reason." class="fa fa-info-circle"></span>
|
||||
</div>
|
||||
<div class="form-group" data-ng-hide="create">
|
||||
<label class="col-sm-2 control-label" for="adminUrl">Admin URL</label>
|
||||
|
|
|
@ -146,7 +146,7 @@ public class SAML2PostBindingResponseBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Response error(String status) throws ConfigurationException, ProcessingException, IOException {
|
||||
public Response buildErrorResponse(String status) throws ConfigurationException, ProcessingException, IOException {
|
||||
Document doc = getErrorResponse(status);
|
||||
return buildResponse(doc);
|
||||
|
||||
|
@ -207,7 +207,7 @@ public class SAML2PostBindingResponseBuilder {
|
|||
return samlResponse;
|
||||
}
|
||||
|
||||
public Response build() throws ConfigurationException, ProcessingException, IOException {
|
||||
public Response buildLoginResponse() throws ConfigurationException, ProcessingException, IOException {
|
||||
Document responseDoc = getResponse();
|
||||
return buildResponse(responseDoc);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.jboss.resteasy.client.ClientRequest;
|
||||
import org.jboss.resteasy.client.ClientResponse;
|
||||
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.ClaimMask;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
|
@ -13,17 +15,31 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.resources.flows.Flows;
|
||||
import org.picketlink.common.constants.GeneralConstants;
|
||||
import org.picketlink.common.constants.JBossSAMLURIConstants;
|
||||
import org.picketlink.common.exceptions.ConfigurationException;
|
||||
import org.picketlink.common.exceptions.ParsingException;
|
||||
import org.picketlink.common.exceptions.ProcessingException;
|
||||
import org.picketlink.common.util.StringUtil;
|
||||
import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request;
|
||||
import org.picketlink.identity.federation.core.saml.v2.constants.X500SAMLProfileConstants;
|
||||
import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil;
|
||||
import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil;
|
||||
import org.picketlink.identity.federation.core.sts.PicketLinkCoreSTS;
|
||||
import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType;
|
||||
import org.picketlink.identity.federation.web.handlers.saml2.SAML2LogOutHandler;
|
||||
import org.picketlink.identity.federation.web.util.PostBindingUtil;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -39,11 +55,8 @@ public class SamlLogin implements LoginProtocol {
|
|||
|
||||
protected RealmModel realm;
|
||||
|
||||
protected HttpRequest request;
|
||||
|
||||
protected UriInfo uriInfo;
|
||||
|
||||
protected ClientConnection clientConnection;
|
||||
|
||||
|
||||
@Override
|
||||
|
@ -58,24 +71,12 @@ public class SamlLogin implements LoginProtocol {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SamlLogin setRequest(HttpRequest request) {
|
||||
this.request = request;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SamlLogin setUriInfo(UriInfo uriInfo) {
|
||||
this.uriInfo = uriInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SamlLogin setClientConnection(ClientConnection clientConnection) {
|
||||
this.clientConnection = clientConnection;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancelLogin(ClientSessionModel clientSession) {
|
||||
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
|
||||
|
@ -100,7 +101,7 @@ public class SamlLogin implements LoginProtocol {
|
|||
.responseIssuer(responseIssuer)
|
||||
.requestIssuer(clientSession.getClient().getClientId());
|
||||
try {
|
||||
return builder.error(status);
|
||||
return builder.buildErrorResponse(status);
|
||||
} catch (Exception e) {
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response");
|
||||
}
|
||||
|
@ -140,7 +141,7 @@ public class SamlLogin implements LoginProtocol {
|
|||
}
|
||||
|
||||
try {
|
||||
return builder.build();
|
||||
return builder.buildLoginResponse();
|
||||
} catch (Exception e) {
|
||||
logger.error("failed", e);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response");
|
||||
|
@ -163,6 +164,66 @@ public class SamlLogin implements LoginProtocol {
|
|||
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
ClientModel client = clientSession.getClient();
|
||||
if (!(client instanceof ApplicationModel)) return;
|
||||
ApplicationModel app = (ApplicationModel)client;
|
||||
if (app.getManagementUrl() == null) return;
|
||||
|
||||
String logoutRequestString = null;
|
||||
try {
|
||||
LogoutRequestType logoutRequest = createLogoutRequest(userSession.getUser(), client);
|
||||
Document logoutRequestDocument = new SAML2Request().convert(logoutRequest);
|
||||
|
||||
byte[] responseBytes = DocumentUtil.getDocumentAsString(logoutRequestDocument).getBytes("UTF-8");
|
||||
logoutRequestString = PostBindingUtil.base64Encode(new String(responseBytes));
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to send saml logout", e);
|
||||
}
|
||||
|
||||
|
||||
String adminUrl = ResourceAdminManager.getManagementUrl(uriInfo.getRequestUri(), app);
|
||||
|
||||
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor();
|
||||
|
||||
|
||||
try {
|
||||
ClientRequest request = executor.createRequest(adminUrl);
|
||||
request.formParameter(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString);
|
||||
request.formParameter(SAML2LogOutHandler.BACK_CHANNEL_LOGOUT, SAML2LogOutHandler.BACK_CHANNEL_LOGOUT);
|
||||
ClientResponse response = null;
|
||||
try {
|
||||
response = request.post();
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to send saml logout", e);
|
||||
}
|
||||
response.releaseConnection();
|
||||
|
||||
} finally {
|
||||
executor.getHttpClient().getConnectionManager().shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private LogoutRequestType createLogoutRequest(UserModel user, ClientModel client) throws ConfigurationException, ProcessingException {
|
||||
LogoutRequestType lort = new SAML2Request().createLogoutRequest(getResponseIssuer(realm));
|
||||
|
||||
NameIDType nameID = new NameIDType();
|
||||
nameID.setValue(user.getUsername());
|
||||
//Deal with NameID Format
|
||||
String nameIDFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get();
|
||||
nameID.setFormat(URI.create(nameIDFormat));
|
||||
lort.setNameID(nameID);
|
||||
|
||||
long assertionValidity = PicketLinkCoreSTS.instance().getConfiguration().getIssuedTokenTimeout();
|
||||
|
||||
lort.setNotOnOrAfter(XMLTimeUtil.add(lort.getIssueInstant(), assertionValidity));
|
||||
lort.setDestination(URI.create(client.getClientId()));
|
||||
return lort;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.picketlink.common.constants.GeneralConstants;
|
|||
import org.picketlink.identity.federation.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.picketlink.identity.federation.saml.v2.SAML2Object;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.AuthnRequestType;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.FormParam;
|
||||
|
@ -94,38 +95,66 @@ public class SamlService {
|
|||
@Path("POST")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response loginPage(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
event.event(EventType.LOGIN);
|
||||
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
if (!checkSsl()) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.SSL_REQUIRED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required");
|
||||
}
|
||||
if (!realm.isEnabled()) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.REALM_DISABLED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled");
|
||||
}
|
||||
|
||||
if (samlRequest == null) {
|
||||
if (samlRequest == null && samlResponse == null) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
|
||||
|
||||
}
|
||||
|
||||
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
|
||||
else return handleSamlResponse(samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected Response handleSamlResponse(String samleResponse, String relayState) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
|
||||
}
|
||||
|
||||
|
||||
protected Response handleSamlRequest(String samlRequest, String relayState) {
|
||||
SAMLDocumentHolder documentHolder = SAMLRequestParser.parsePostBinding(samlRequest);
|
||||
if (documentHolder == null) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
|
||||
}
|
||||
|
||||
SAML2Object samlObject = documentHolder.getSamlObject();
|
||||
if (!(samlObject instanceof AuthnRequestType)) {
|
||||
|
||||
if (samlObject instanceof AuthnRequestType) {
|
||||
event.event(EventType.LOGIN);
|
||||
// Get the SAML Request Message
|
||||
AuthnRequestType requestAbstractType = (AuthnRequestType) samlObject;
|
||||
return loginRequest(relayState, requestAbstractType);
|
||||
} else if (samlObject instanceof LogoutRequestType) {
|
||||
event.event(EventType.LOGOUT);
|
||||
LogoutRequestType requestAbstractType = (LogoutRequestType) samlObject;
|
||||
return logoutRequest(relayState, requestAbstractType);
|
||||
|
||||
} else {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
|
||||
}
|
||||
}
|
||||
|
||||
// Get the SAML Request Message
|
||||
AuthnRequestType requestAbstractType = (AuthnRequestType) samlObject;
|
||||
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType) {
|
||||
String issuer = requestAbstractType.getIssuer().getValue();
|
||||
ClientModel client = realm.findClient(issuer);
|
||||
|
||||
|
@ -189,29 +218,42 @@ public class SamlService {
|
|||
return forms.createLogin();
|
||||
}
|
||||
|
||||
protected Response logoutRequest(String relayState, LogoutRequestType requestAbstractType) {
|
||||
String issuer = requestAbstractType.getIssuer().getValue();
|
||||
ClientModel client = realm.findClient(issuer);
|
||||
|
||||
/**
|
||||
* Logout user session. User must be logged in via a session cookie.
|
||||
*
|
||||
* @param redirectUri
|
||||
* @return
|
||||
*/
|
||||
@Path("logout")
|
||||
@GET
|
||||
@NoCache
|
||||
public Response logout(final @QueryParam("shit") String redirectUri) {
|
||||
event.event(EventType.LOGOUT);
|
||||
if (redirectUri != null) {
|
||||
event.detail(Details.REDIRECT_URI, redirectUri);
|
||||
if (client == null) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester.");
|
||||
}
|
||||
|
||||
if (!client.isEnabled()) {
|
||||
event.error(Errors.CLIENT_DISABLED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled.");
|
||||
}
|
||||
if ((client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Bearer-only applications are not allowed to initiate browser login");
|
||||
}
|
||||
if (client.isDirectGrantsOnly()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login");
|
||||
}
|
||||
|
||||
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
|
||||
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
|
||||
if (authResult != null) {
|
||||
logout(authResult.getSession());
|
||||
}
|
||||
|
||||
String redirectUri = null;
|
||||
|
||||
if (client instanceof ApplicationModel) {
|
||||
redirectUri = ((ApplicationModel)client).getBaseUrl();
|
||||
}
|
||||
|
||||
if (redirectUri != null) {
|
||||
String validatedRedirect = OpenIDConnectService.verifyRealmRedirectUri(uriInfo, redirectUri, realm);
|
||||
String validatedRedirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri, realm, client);;
|
||||
if (validatedRedirect == null) {
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri.");
|
||||
}
|
||||
|
@ -219,6 +261,7 @@ public class SamlService {
|
|||
} else {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void logout(UserSessionModel userSession) {
|
||||
|
@ -233,14 +276,4 @@ public class SamlService {
|
|||
return !realm.getSslRequired().isRequired(clientConnection);
|
||||
}
|
||||
}
|
||||
|
||||
private Response createError(String error, String errorDescription, Response.Status status) {
|
||||
Map<String, String> e = new HashMap<String, String>();
|
||||
e.put(OAuth2Constants.ERROR, error);
|
||||
if (errorDescription != null) {
|
||||
e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
|
||||
}
|
||||
return Response.status(status).entity(e).type("application/json").build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,14 +22,12 @@ public interface LoginProtocol extends Provider {
|
|||
|
||||
LoginProtocol setRealm(RealmModel realm);
|
||||
|
||||
LoginProtocol setRequest(HttpRequest request);
|
||||
|
||||
LoginProtocol setUriInfo(UriInfo uriInfo);
|
||||
|
||||
LoginProtocol setClientConnection(ClientConnection clientConnection);
|
||||
|
||||
Response cancelLogin(ClientSessionModel clientSession);
|
||||
Response invalidSessionError(ClientSessionModel clientSession);
|
||||
Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode);
|
||||
Response consentDenied(ClientSessionModel clientSession);
|
||||
|
||||
void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
|
||||
}
|
||||
|
|
|
@ -22,15 +22,18 @@
|
|||
package org.keycloak.protocol.oidc;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
@ -56,19 +59,12 @@ public class OpenIDConnect implements LoginProtocol {
|
|||
|
||||
protected RealmModel realm;
|
||||
|
||||
protected HttpRequest request;
|
||||
|
||||
protected UriInfo uriInfo;
|
||||
|
||||
protected ClientConnection clientConnection;
|
||||
|
||||
public OpenIDConnect(KeycloakSession session, RealmModel realm, HttpRequest request, UriInfo uriInfo,
|
||||
ClientConnection clientConnection) {
|
||||
public OpenIDConnect(KeycloakSession session, RealmModel realm, UriInfo uriInfo) {
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
this.request = request;
|
||||
this.uriInfo = uriInfo;
|
||||
this.clientConnection = clientConnection;
|
||||
}
|
||||
|
||||
public OpenIDConnect() {
|
||||
|
@ -86,24 +82,12 @@ public class OpenIDConnect implements LoginProtocol {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenIDConnect setRequest(HttpRequest request) {
|
||||
this.request = request;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenIDConnect setUriInfo(UriInfo uriInfo) {
|
||||
this.uriInfo = uriInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenIDConnect setClientConnection(ClientConnection clientConnection) {
|
||||
this.clientConnection = clientConnection;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancelLogin(ClientSessionModel clientSession) {
|
||||
String redirect = clientSession.getRedirectUri();
|
||||
|
@ -151,6 +135,19 @@ public class OpenIDConnect implements LoginProtocol {
|
|||
return Response.status(302).location(redirectUri.build()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
if (!(clientSession.getClient() instanceof ApplicationModel)) return;
|
||||
ApplicationModel app = (ApplicationModel)clientSession.getClient();
|
||||
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor();
|
||||
|
||||
try {
|
||||
new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, app, null, userSession.getId(), executor, 0);
|
||||
} finally {
|
||||
executor.getHttpClient().getConnectionManager().shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
|
|
@ -800,7 +800,7 @@ public class OpenIDConnectService {
|
|||
if (response != null) return response;
|
||||
|
||||
if (prompt != null && prompt.equals("none")) {
|
||||
OpenIDConnect oauth = new OpenIDConnect(session, realm, request, uriInfo, clientConnection);
|
||||
OpenIDConnect oauth = new OpenIDConnect(session, realm, uriInfo);
|
||||
return oauth.cancelLogin(clientSession);
|
||||
}
|
||||
|
||||
|
|
|
@ -86,7 +86,17 @@ public class AuthenticationManager {
|
|||
expireIdentityCookie(realm, uriInfo, connection);
|
||||
expireRememberMeCookie(realm, uriInfo, connection);
|
||||
|
||||
new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), userSession);
|
||||
for (ClientSessionModel clientSession : userSession.getClientSessions()) {
|
||||
ClientModel client = clientSession.getClient();
|
||||
if (client instanceof ApplicationModel) {
|
||||
String authMethod = clientSession.getAuthMethod();
|
||||
if (authMethod == null) continue; // must be a keycloak service like account
|
||||
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
|
||||
protocol.setRealm(realm)
|
||||
.setUriInfo(uriInfo);
|
||||
protocol.backchannelLogout(userSession, clientSession);
|
||||
}
|
||||
}
|
||||
|
||||
session.sessions().removeUserSession(realm, userSession);
|
||||
}
|
||||
|
@ -235,9 +245,7 @@ public class AuthenticationManager {
|
|||
if (userSession.isRememberMe()) createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection);
|
||||
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
|
||||
protocol.setRealm(realm)
|
||||
.setRequest(request)
|
||||
.setUriInfo(uriInfo)
|
||||
.setClientConnection(clientConnection);
|
||||
.setUriInfo(uriInfo);
|
||||
return protocol.authenticated(userSession, new ClientSessionCode(realm, clientSession));
|
||||
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ public class ResourceAdminManager {
|
|||
|
||||
}
|
||||
|
||||
protected String getManagementUrl(URI requestUri, ApplicationModel application) {
|
||||
public static String getManagementUrl(URI requestUri, ApplicationModel application) {
|
||||
String mgmtUrl = application.getManagementUrl();
|
||||
if (mgmtUrl == null || mgmtUrl.equals("")) {
|
||||
return null;
|
||||
|
@ -231,7 +231,7 @@ public class ResourceAdminManager {
|
|||
}
|
||||
|
||||
|
||||
protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, String session, ApacheHttpClient4Executor client, int notBefore) {
|
||||
public boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, String session, ApacheHttpClient4Executor client, int notBefore) {
|
||||
String managementUrl = getManagementUrl(requestUri, resource);
|
||||
if (managementUrl != null) {
|
||||
LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), user, session, notBefore);
|
||||
|
|
|
@ -309,9 +309,7 @@ public class LoginActionsService {
|
|||
event.error(Errors.REJECTED_BY_USER);
|
||||
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
|
||||
protocol.setRealm(realm)
|
||||
.setRequest(request)
|
||||
.setUriInfo(uriInfo)
|
||||
.setClientConnection(clientConnection);
|
||||
.setUriInfo(uriInfo);
|
||||
return protocol.cancelLogin(clientSession);
|
||||
}
|
||||
|
||||
|
@ -565,9 +563,7 @@ public class LoginActionsService {
|
|||
|
||||
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
|
||||
protocol.setRealm(realm)
|
||||
.setRequest(request)
|
||||
.setUriInfo(uriInfo)
|
||||
.setClientConnection(clientConnection);
|
||||
.setUriInfo(uriInfo);
|
||||
if (formData.containsKey("cancel")) {
|
||||
event.error(Errors.REJECTED_BY_USER);
|
||||
return protocol.consentDenied(clientSession);
|
||||
|
|
|
@ -1,421 +1,429 @@
|
|||
/*
|
||||
* JBoss, Home of Professional Open Source.
|
||||
* Copyright 2012, Red Hat, Inc., and individual contributors
|
||||
* as indicated by the @author tags. See the copyright.txt file in the
|
||||
* distribution for a full listing of individual contributors.
|
||||
*
|
||||
* This is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this software; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
||||
*/
|
||||
package org.keycloak.testsuite.adapter;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.Version;
|
||||
import org.keycloak.adapters.AdapterConstants;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.OpenIDConnectService;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.adapters.action.SessionStats;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.resources.admin.AdminRoot;
|
||||
import org.keycloak.testsuite.OAuthClient;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
|
||||
import org.keycloak.testsuite.rule.WebResource;
|
||||
import org.keycloak.testsuite.rule.WebRule;
|
||||
import org.keycloak.testutils.KeycloakServer;
|
||||
import org.keycloak.util.BasicAuthHelper;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.Form;
|
||||
import javax.ws.rs.core.GenericType;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Tests Undertow Adapter
|
||||
*
|
||||
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
|
||||
*/
|
||||
public class AdapterTest {
|
||||
|
||||
public static final String LOGIN_URL = OpenIDConnectService.loginPageUrl(UriBuilder.fromUri("http://localhost:8081/auth")).build("demo").toString();
|
||||
public static PublicKey realmPublicKey;
|
||||
@ClassRule
|
||||
public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() {
|
||||
@Override
|
||||
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
|
||||
RealmRepresentation representation = KeycloakServer.loadJson(getClass().getResourceAsStream("/adapter-test/demorealm.json"), RealmRepresentation.class);
|
||||
RealmModel realm = manager.importRealm(representation);
|
||||
|
||||
realmPublicKey = realm.getPublicKey();
|
||||
|
||||
URL url = getClass().getResource("/adapter-test/cust-app-keycloak.json");
|
||||
deployApplication("customer-portal", "/customer-portal", CustomerServlet.class, url.getPath(), "user");
|
||||
url = getClass().getResource("/adapter-test/secure-portal-keycloak.json");
|
||||
deployApplication("secure-portal", "/secure-portal", CallAuthenticatedServlet.class, url.getPath(), "user", false);
|
||||
url = getClass().getResource("/adapter-test/customer-db-keycloak.json");
|
||||
deployApplication("customer-db", "/customer-db", CustomerDatabaseServlet.class, url.getPath(), "user");
|
||||
url = getClass().getResource("/adapter-test/product-keycloak.json");
|
||||
deployApplication("product-portal", "/product-portal", ProductServlet.class, url.getPath(), "user");
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private static String createToken() {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
|
||||
RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
|
||||
ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
|
||||
TokenManager tm = new TokenManager();
|
||||
UserModel admin = session.users().getUserByUsername("admin", adminRealm);
|
||||
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false);
|
||||
AccessToken token = tm.createClientAccessToken(tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession);
|
||||
return tm.encodeToken(adminRealm, token);
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Rule
|
||||
public WebRule webRule = new WebRule(this);
|
||||
|
||||
@WebResource
|
||||
protected WebDriver driver;
|
||||
|
||||
@WebResource
|
||||
protected OAuthClient oauth;
|
||||
|
||||
@WebResource
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Test
|
||||
public void testLoginSSOAndLogout() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/product-portal");
|
||||
pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
|
||||
|
||||
// View stats
|
||||
String adminToken = createToken();
|
||||
|
||||
Client client = ClientBuilder.newClient();
|
||||
UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
|
||||
WebTarget adminTarget = client.target(AdminRoot.realmsUrl(authBase)).path("demo");
|
||||
Map<String, SessionStats> stats = adminTarget.path("session-stats").request()
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
|
||||
.get(new GenericType<Map<String, SessionStats>>() {
|
||||
});
|
||||
|
||||
SessionStats custStats = stats.get("customer-portal");
|
||||
Assert.assertNotNull(custStats);
|
||||
Assert.assertEquals(1, custStats.getActiveSessions());
|
||||
SessionStats prodStats = stats.get("product-portal");
|
||||
Assert.assertNotNull(prodStats);
|
||||
Assert.assertEquals(1, prodStats.getActiveSessions());
|
||||
|
||||
client.close();
|
||||
|
||||
|
||||
// test logout
|
||||
|
||||
String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/customer-portal").build("demo").toString();
|
||||
driver.navigate().to(logoutUri);
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServletRequestLogout() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/product-portal");
|
||||
pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
|
||||
|
||||
// back
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
// test logout
|
||||
|
||||
driver.navigate().to("http://localhost:8081/customer-portal/logout");
|
||||
|
||||
|
||||
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
String currentUrl = driver.getCurrentUrl();
|
||||
Assert.assertTrue(currentUrl.startsWith(LOGIN_URL));
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginSSOIdle() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
RealmModel realm = session.realms().getRealmByName("demo");
|
||||
int originalIdle = realm.getSsoSessionIdleTimeout();
|
||||
realm.setSsoSessionIdleTimeout(1);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
realm = session.realms().getRealmByName("demo");
|
||||
realm.setSsoSessionIdleTimeout(originalIdle);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginSSOIdleRemoveExpiredUserSessions() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
RealmModel realm = session.realms().getRealmByName("demo");
|
||||
int originalIdle = realm.getSsoSessionIdleTimeout();
|
||||
realm.setSsoSessionIdleTimeout(1);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
realm = session.realms().getRealmByName("demo");
|
||||
session.sessions().removeExpiredUserSessions(realm);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
realm = session.realms().getRealmByName("demo");
|
||||
realm.setSsoSessionIdleTimeout(originalIdle);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginSSOMax() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
RealmModel realm = session.realms().getRealmByName("demo");
|
||||
int original = realm.getSsoSessionMaxLifespan();
|
||||
realm.setSsoSessionMaxLifespan(1);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
realm = session.realms().getRealmByName("demo");
|
||||
realm.setSsoSessionMaxLifespan(original);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* KEYCLOAK-518
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testNullBearerToken() throws Exception {
|
||||
Client client = ClientBuilder.newClient();
|
||||
WebTarget target = client.target("http://localhost:8081/customer-db");
|
||||
Response response = target.request().get();
|
||||
Assert.assertEquals(401, response.getStatus());
|
||||
response.close();
|
||||
response = target.request().header(HttpHeaders.AUTHORIZATION, "Bearer null").get();
|
||||
Assert.assertEquals(401, response.getStatus());
|
||||
response.close();
|
||||
client.close();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* KEYCLOAK-518
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testBadUser() throws Exception {
|
||||
Client client = ClientBuilder.newClient();
|
||||
UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
|
||||
URI uri = OpenIDConnectService.grantAccessTokenUrl(builder).build("demo");
|
||||
WebTarget target = client.target(uri);
|
||||
String header = BasicAuthHelper.createHeader("customer-portal", "password");
|
||||
Form form = new Form();
|
||||
form.param("username", "monkey@redhat.com")
|
||||
.param("password", "password");
|
||||
Response response = target.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, header)
|
||||
.post(Entity.form(form));
|
||||
Assert.assertEquals(400, response.getStatus());
|
||||
response.close();
|
||||
client.close();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVersion() throws Exception {
|
||||
Client client = ClientBuilder.newClient();
|
||||
WebTarget target = client.target(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT).path("version");
|
||||
Version version = target.request().get(Version.class);
|
||||
Assert.assertNotNull(version);
|
||||
Assert.assertNotNull(version.getVersion());
|
||||
Assert.assertNotNull(version.getBuildTime());
|
||||
Assert.assertNotEquals(version.getVersion(), Version.UNKNOWN);
|
||||
Assert.assertNotEquals(version.getBuildTime(), Version.UNKNOWN);
|
||||
|
||||
Version version2 = client.target("http://localhost:8081/secure-portal").path(AdapterConstants.K_VERSION).request().get(Version.class);
|
||||
Assert.assertNotNull(version2);
|
||||
Assert.assertNotNull(version2.getVersion());
|
||||
Assert.assertNotNull(version2.getBuildTime());
|
||||
Assert.assertEquals(version.getVersion(), version2.getVersion());
|
||||
Assert.assertEquals(version.getBuildTime(), version2.getBuildTime());
|
||||
client.close();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testAuthenticated() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/secure-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/secure-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
// test logout
|
||||
|
||||
String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/secure-portal").build("demo").toString();
|
||||
driver.navigate().to(logoutUri);
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
driver.navigate().to("http://localhost:8081/secure-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
/*
|
||||
* JBoss, Home of Professional Open Source.
|
||||
* Copyright 2012, Red Hat, Inc., and individual contributors
|
||||
* as indicated by the @author tags. See the copyright.txt file in the
|
||||
* distribution for a full listing of individual contributors.
|
||||
*
|
||||
* This is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this software; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
||||
*/
|
||||
package org.keycloak.testsuite.adapter;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.Version;
|
||||
import org.keycloak.adapters.AdapterConstants;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.OpenIDConnectService;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.adapters.action.SessionStats;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.resources.admin.AdminRoot;
|
||||
import org.keycloak.testsuite.OAuthClient;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
|
||||
import org.keycloak.testsuite.rule.WebResource;
|
||||
import org.keycloak.testsuite.rule.WebRule;
|
||||
import org.keycloak.testutils.KeycloakServer;
|
||||
import org.keycloak.util.BasicAuthHelper;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.Form;
|
||||
import javax.ws.rs.core.GenericType;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Tests Undertow Adapter
|
||||
*
|
||||
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
|
||||
*/
|
||||
public class AdapterTest {
|
||||
|
||||
public static final String LOGIN_URL = OpenIDConnectService.loginPageUrl(UriBuilder.fromUri("http://localhost:8081/auth")).build("demo").toString();
|
||||
public static PublicKey realmPublicKey;
|
||||
@ClassRule
|
||||
public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() {
|
||||
@Override
|
||||
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
|
||||
RealmRepresentation representation = KeycloakServer.loadJson(getClass().getResourceAsStream("/adapter-test/demorealm.json"), RealmRepresentation.class);
|
||||
RealmModel realm = manager.importRealm(representation);
|
||||
|
||||
realmPublicKey = realm.getPublicKey();
|
||||
|
||||
URL url = getClass().getResource("/adapter-test/cust-app-keycloak.json");
|
||||
deployApplication("customer-portal", "/customer-portal", CustomerServlet.class, url.getPath(), "user");
|
||||
url = getClass().getResource("/adapter-test/secure-portal-keycloak.json");
|
||||
deployApplication("secure-portal", "/secure-portal", CallAuthenticatedServlet.class, url.getPath(), "user", false);
|
||||
url = getClass().getResource("/adapter-test/customer-db-keycloak.json");
|
||||
deployApplication("customer-db", "/customer-db", CustomerDatabaseServlet.class, url.getPath(), "user");
|
||||
url = getClass().getResource("/adapter-test/product-keycloak.json");
|
||||
deployApplication("product-portal", "/product-portal", ProductServlet.class, url.getPath(), "user");
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private static String createToken() {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
|
||||
RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
|
||||
ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
|
||||
TokenManager tm = new TokenManager();
|
||||
UserModel admin = session.users().getUserByUsername("admin", adminRealm);
|
||||
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false);
|
||||
AccessToken token = tm.createClientAccessToken(tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession);
|
||||
return tm.encodeToken(adminRealm, token);
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Rule
|
||||
public WebRule webRule = new WebRule(this);
|
||||
|
||||
@WebResource
|
||||
protected WebDriver driver;
|
||||
|
||||
@WebResource
|
||||
protected OAuthClient oauth;
|
||||
|
||||
@WebResource
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Test
|
||||
public void testLoginSSOAndLogout() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/product-portal");
|
||||
pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
|
||||
|
||||
// View stats
|
||||
String adminToken = createToken();
|
||||
|
||||
Client client = ClientBuilder.newClient();
|
||||
UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
|
||||
WebTarget adminTarget = client.target(AdminRoot.realmsUrl(authBase)).path("demo");
|
||||
Map<String, SessionStats> stats = adminTarget.path("session-stats").request()
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
|
||||
.get(new GenericType<Map<String, SessionStats>>() {
|
||||
});
|
||||
|
||||
SessionStats custStats = stats.get("customer-portal");
|
||||
Assert.assertNotNull(custStats);
|
||||
Assert.assertEquals(1, custStats.getActiveSessions());
|
||||
SessionStats prodStats = stats.get("product-portal");
|
||||
Assert.assertNotNull(prodStats);
|
||||
Assert.assertEquals(1, prodStats.getActiveSessions());
|
||||
|
||||
client.close();
|
||||
|
||||
|
||||
// test logout
|
||||
|
||||
String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/customer-portal").build("demo").toString();
|
||||
driver.navigate().to(logoutUri);
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServletRequestLogout() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/product-portal");
|
||||
pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
|
||||
|
||||
// back
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
// test logout
|
||||
|
||||
driver.navigate().to("http://localhost:8081/customer-portal/logout");
|
||||
|
||||
|
||||
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
String currentUrl = driver.getCurrentUrl();
|
||||
Assert.assertTrue(currentUrl.startsWith(LOGIN_URL));
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginSSOIdle() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
RealmModel realm = session.realms().getRealmByName("demo");
|
||||
int originalIdle = realm.getSsoSessionIdleTimeout();
|
||||
realm.setSsoSessionIdleTimeout(1);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
realm = session.realms().getRealmByName("demo");
|
||||
realm.setSsoSessionIdleTimeout(originalIdle);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginSSOIdleRemoveExpiredUserSessions() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
RealmModel realm = session.realms().getRealmByName("demo");
|
||||
int originalIdle = realm.getSsoSessionIdleTimeout();
|
||||
realm.setSsoSessionIdleTimeout(1);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
realm = session.realms().getRealmByName("demo");
|
||||
session.sessions().removeExpiredUserSessions(realm);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
realm = session.realms().getRealmByName("demo");
|
||||
// need to cleanup so other tests don't fail, so invalidate http sessions on remote clients.
|
||||
UserModel user = session.users().getUserByUsername("bburke@redhat.com", realm);
|
||||
new ResourceAdminManager().logoutUser(null, realm, user.getId(), null);
|
||||
realm.setSsoSessionIdleTimeout(originalIdle);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginSSOMax() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
RealmModel realm = session.realms().getRealmByName("demo");
|
||||
int original = realm.getSsoSessionMaxLifespan();
|
||||
realm.setSsoSessionMaxLifespan(1);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
|
||||
// test SSO
|
||||
driver.navigate().to("http://localhost:8081/product-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
session = keycloakRule.startSession();
|
||||
realm = session.realms().getRealmByName("demo");
|
||||
realm.setSsoSessionMaxLifespan(original);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* KEYCLOAK-518
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testNullBearerToken() throws Exception {
|
||||
Client client = ClientBuilder.newClient();
|
||||
WebTarget target = client.target("http://localhost:8081/customer-db");
|
||||
Response response = target.request().get();
|
||||
Assert.assertEquals(401, response.getStatus());
|
||||
response.close();
|
||||
response = target.request().header(HttpHeaders.AUTHORIZATION, "Bearer null").get();
|
||||
Assert.assertEquals(401, response.getStatus());
|
||||
response.close();
|
||||
client.close();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* KEYCLOAK-518
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testBadUser() throws Exception {
|
||||
Client client = ClientBuilder.newClient();
|
||||
UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
|
||||
URI uri = OpenIDConnectService.grantAccessTokenUrl(builder).build("demo");
|
||||
WebTarget target = client.target(uri);
|
||||
String header = BasicAuthHelper.createHeader("customer-portal", "password");
|
||||
Form form = new Form();
|
||||
form.param("username", "monkey@redhat.com")
|
||||
.param("password", "password");
|
||||
Response response = target.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, header)
|
||||
.post(Entity.form(form));
|
||||
Assert.assertEquals(400, response.getStatus());
|
||||
response.close();
|
||||
client.close();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVersion() throws Exception {
|
||||
Client client = ClientBuilder.newClient();
|
||||
WebTarget target = client.target(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT).path("version");
|
||||
Version version = target.request().get(Version.class);
|
||||
Assert.assertNotNull(version);
|
||||
Assert.assertNotNull(version.getVersion());
|
||||
Assert.assertNotNull(version.getBuildTime());
|
||||
Assert.assertNotEquals(version.getVersion(), Version.UNKNOWN);
|
||||
Assert.assertNotEquals(version.getBuildTime(), Version.UNKNOWN);
|
||||
|
||||
Version version2 = client.target("http://localhost:8081/secure-portal").path(AdapterConstants.K_VERSION).request().get(Version.class);
|
||||
Assert.assertNotNull(version2);
|
||||
Assert.assertNotNull(version2.getVersion());
|
||||
Assert.assertNotNull(version2.getBuildTime());
|
||||
Assert.assertEquals(version.getVersion(), version2.getVersion());
|
||||
Assert.assertEquals(version.getBuildTime(), version2.getBuildTime());
|
||||
client.close();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testAuthenticated() throws Exception {
|
||||
// test login to customer-portal which does a bearer request to customer-db
|
||||
driver.navigate().to("http://localhost:8081/secure-portal");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
System.out.println("Current url: " + driver.getCurrentUrl());
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/secure-portal");
|
||||
String pageSource = driver.getPageSource();
|
||||
System.out.println(pageSource);
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
|
||||
// test logout
|
||||
|
||||
String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/secure-portal").build("demo").toString();
|
||||
driver.navigate().to(logoutUri);
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
driver.navigate().to("http://localhost:8081/secure-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
"name": "http://localhost:8080/sales-post/",
|
||||
"enabled": true,
|
||||
"fullScopeAllowed": true,
|
||||
"baseUrl": "http://localhost:8080/sales-post/",
|
||||
"adminUrl": "http://localhost:8080/sales-post/",
|
||||
"redirectUris": [
|
||||
"http://localhost:8080/sales-post/*"
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue