commit
89d47a392b
8 changed files with 533 additions and 215 deletions
|
@ -9,7 +9,7 @@
|
|||
</li>
|
||||
<li data-ng-show="access.viewUsers" data-ng-class="(path[2] == 'users' || path[1] == 'user') && 'active'"><a href="#/realms/{{realm.realm}}/users">Users</a>
|
||||
</li>
|
||||
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'roles' || (path[1] == 'role' & path[3] != 'applications') && 'active'"><a href="#/realms/{{realm.realm}}/roles">Roles</a>
|
||||
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'roles' || (path[1] == 'role' && path[3] != 'applications')) && 'active'"><a href="#/realms/{{realm.realm}}/roles">Roles</a>
|
||||
</li>
|
||||
<li data-ng-show="access.viewApplications" data-ng-class="(path[2] == 'applications' || path[1] == 'application' || path[3] == 'applications') && 'active'"><a href="#/realms/{{realm.realm}}/applications">Applications</a></li>
|
||||
<li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'oauth-clients' || path[1] == 'oauth-client') && 'active'"><a href="#/realms/{{realm.realm}}/oauth-clients">OAuth Clients</a></li>
|
||||
|
|
|
@ -173,7 +173,7 @@ public class AuthenticationManager {
|
|||
public AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers, boolean checkActive) {
|
||||
logger.info("authenticateIdentityCookie");
|
||||
Cookie cookie = headers.getCookies().get(KEYCLOAK_IDENTITY_COOKIE);
|
||||
if (cookie == null) {
|
||||
if (cookie == null || "".equals(cookie.getValue())) {
|
||||
logger.infov("authenticateCookie could not find cookie: {0}", KEYCLOAK_IDENTITY_COOKIE);
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ public class UserFederationResource {
|
|||
@GET
|
||||
@NoCache
|
||||
@Path("instances/{id}")
|
||||
@Consumes("application/json")
|
||||
@Produces("application/json")
|
||||
public UserFederationProviderRepresentation getProviderInstance(@PathParam("id") String id) {
|
||||
logger.info("getProvider");
|
||||
auth.requireView();
|
||||
|
|
|
@ -1,199 +1,203 @@
|
|||
/*
|
||||
* 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.services.resources.flows;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.audit.Audit;
|
||||
import org.keycloak.audit.Details;
|
||||
import org.keycloak.audit.EventType;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredCredentialModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserModel.RequiredAction;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.managers.AccessCode;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.TokenManager;
|
||||
|
||||
import javax.ws.rs.core.Cookie;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class OAuthFlows {
|
||||
|
||||
private static final Logger log = Logger.getLogger(OAuthFlows.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
private final RealmModel realm;
|
||||
|
||||
private final HttpRequest request;
|
||||
|
||||
private final UriInfo uriInfo;
|
||||
|
||||
private ClientConnection clientConnection;
|
||||
private final AuthenticationManager authManager;
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
OAuthFlows(KeycloakSession session, RealmModel realm, HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection, AuthenticationManager authManager,
|
||||
TokenManager tokenManager) {
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
this.request = request;
|
||||
this.uriInfo = uriInfo;
|
||||
this.clientConnection = clientConnection;
|
||||
this.authManager = authManager;
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
public Response redirectAccessCode(AccessCode accessCode, UserSessionModel userSession, String state, String redirect) {
|
||||
String code = accessCode.getCode();
|
||||
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code);
|
||||
log.debugv("redirectAccessCode: state: {0}", state);
|
||||
if (state != null)
|
||||
redirectUri.queryParam(OAuth2Constants.STATE, state);
|
||||
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
|
||||
Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
|
||||
|
||||
Cookie sessionCookie = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_SESSION_COOKIE);
|
||||
if (sessionCookie != null) {
|
||||
String oldSessionId = sessionCookie.getValue().split("/")[2];
|
||||
if (!oldSessionId.equals(userSession.getId())) {
|
||||
UserSessionModel oldSession = session.sessions().getUserSession(realm, oldSessionId);
|
||||
if (oldSession != null) {
|
||||
log.debugv("Removing old user session: session: {0}", oldSessionId);
|
||||
session.sessions().removeUserSession(realm, oldSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// refresh the cookies!
|
||||
authManager.createLoginCookie(realm, accessCode.getUser(), userSession, uriInfo, clientConnection);
|
||||
if (userSession.isRememberMe()) authManager.createRememberMeCookie(realm, uriInfo, clientConnection);
|
||||
return location.build();
|
||||
}
|
||||
|
||||
public Response redirectError(ClientModel client, String error, String state, String redirect) {
|
||||
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, error);
|
||||
if (state != null) {
|
||||
redirectUri.queryParam(OAuth2Constants.STATE, state);
|
||||
}
|
||||
return Response.status(302).location(redirectUri.build()).build();
|
||||
}
|
||||
|
||||
public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, UserSessionModel session, Audit audit) {
|
||||
isTotpConfigurationRequired(user);
|
||||
isEmailVerificationRequired(user);
|
||||
|
||||
boolean isResource = client instanceof ApplicationModel;
|
||||
AccessCode accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, this.session, realm, client, user, session);
|
||||
|
||||
log.debugv("processAccessCode: isResource: {0}", isResource);
|
||||
log.debugv("processAccessCode: go to oauth page?: {0}",
|
||||
!isResource);
|
||||
|
||||
audit.detail(Details.CODE_ID, accessCode.getCodeId());
|
||||
|
||||
Set<RequiredAction> requiredActions = user.getRequiredActions();
|
||||
if (!requiredActions.isEmpty()) {
|
||||
RequiredAction action = user.getRequiredActions().iterator().next();
|
||||
accessCode.setRequiredAction(action);
|
||||
|
||||
if (action.equals(RequiredAction.VERIFY_EMAIL)) {
|
||||
audit.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
|
||||
}
|
||||
|
||||
return Flows.forms(this.session, realm, client, uriInfo).setAccessCode(accessCode.getCode()).setUser(user)
|
||||
.createResponse(action);
|
||||
}
|
||||
|
||||
if (!isResource) {
|
||||
accessCode.setAction(ClientSessionModel.Action.OAUTH_GRANT);
|
||||
|
||||
List<RoleModel> realmRoles = new LinkedList<RoleModel>();
|
||||
MultivaluedMap<String, RoleModel> resourceRoles = new MultivaluedMapImpl<String, RoleModel>();
|
||||
for (RoleModel r : accessCode.getRequestedRoles()) {
|
||||
if (r.getContainer() instanceof RealmModel) {
|
||||
realmRoles.add(r);
|
||||
} else {
|
||||
resourceRoles.add(((ApplicationModel) r.getContainer()).getName(), r);
|
||||
}
|
||||
}
|
||||
|
||||
return Flows.forms(this.session, realm, client, uriInfo)
|
||||
.setAccessCode(accessCode.getCode())
|
||||
.setAccessRequest(realmRoles, resourceRoles)
|
||||
.setClient(client)
|
||||
.createOAuthGrant();
|
||||
}
|
||||
|
||||
if (redirect != null) {
|
||||
audit.success();
|
||||
|
||||
accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN);
|
||||
return redirectAccessCode(accessCode, session, state, redirect);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Response forwardToSecurityFailure(String message) {
|
||||
return Flows.forms(session, realm, null, uriInfo).setError(message).createErrorPage();
|
||||
}
|
||||
|
||||
private void isTotpConfigurationRequired(UserModel user) {
|
||||
for (RequiredCredentialModel c : realm.getRequiredCredentials()) {
|
||||
if (c.getType().equals(CredentialRepresentation.TOTP) && !user.isTotp()) {
|
||||
user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
|
||||
log.debug("User is required to configure totp");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void isEmailVerificationRequired(UserModel user) {
|
||||
if (realm.isVerifyEmail() && !user.isEmailVerified()) {
|
||||
user.addRequiredAction(RequiredAction.VERIFY_EMAIL);
|
||||
log.debug("User is required to verify email");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
* 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.services.resources.flows;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.audit.Audit;
|
||||
import org.keycloak.audit.Details;
|
||||
import org.keycloak.audit.EventType;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredCredentialModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserModel.RequiredAction;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.managers.AccessCode;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.TokenManager;
|
||||
|
||||
import javax.ws.rs.core.Cookie;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class OAuthFlows {
|
||||
|
||||
private static final Logger log = Logger.getLogger(OAuthFlows.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
private final RealmModel realm;
|
||||
|
||||
private final HttpRequest request;
|
||||
|
||||
private final UriInfo uriInfo;
|
||||
|
||||
private ClientConnection clientConnection;
|
||||
private final AuthenticationManager authManager;
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
OAuthFlows(KeycloakSession session, RealmModel realm, HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection, AuthenticationManager authManager,
|
||||
TokenManager tokenManager) {
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
this.request = request;
|
||||
this.uriInfo = uriInfo;
|
||||
this.clientConnection = clientConnection;
|
||||
this.authManager = authManager;
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
public Response redirectAccessCode(AccessCode accessCode, UserSessionModel userSession, String state, String redirect) {
|
||||
String code = accessCode.getCode();
|
||||
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code);
|
||||
log.debugv("redirectAccessCode: state: {0}", state);
|
||||
if (state != null)
|
||||
redirectUri.queryParam(OAuth2Constants.STATE, state);
|
||||
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
|
||||
Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
|
||||
|
||||
Cookie sessionCookie = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_SESSION_COOKIE);
|
||||
if (sessionCookie != null) {
|
||||
|
||||
String[] split = sessionCookie.getValue().split("/");
|
||||
if (split.length >= 3) {
|
||||
String oldSessionId = split[2];
|
||||
if (!oldSessionId.equals(userSession.getId())) {
|
||||
UserSessionModel oldSession = session.sessions().getUserSession(realm, oldSessionId);
|
||||
if (oldSession != null) {
|
||||
log.debugv("Removing old user session: session: {0}", oldSessionId);
|
||||
session.sessions().removeUserSession(realm, oldSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// refresh the cookies!
|
||||
authManager.createLoginCookie(realm, accessCode.getUser(), userSession, uriInfo, clientConnection);
|
||||
if (userSession.isRememberMe()) authManager.createRememberMeCookie(realm, uriInfo, clientConnection);
|
||||
return location.build();
|
||||
}
|
||||
|
||||
public Response redirectError(ClientModel client, String error, String state, String redirect) {
|
||||
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, error);
|
||||
if (state != null) {
|
||||
redirectUri.queryParam(OAuth2Constants.STATE, state);
|
||||
}
|
||||
return Response.status(302).location(redirectUri.build()).build();
|
||||
}
|
||||
|
||||
public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, UserSessionModel session, Audit audit) {
|
||||
isTotpConfigurationRequired(user);
|
||||
isEmailVerificationRequired(user);
|
||||
|
||||
boolean isResource = client instanceof ApplicationModel;
|
||||
AccessCode accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, this.session, realm, client, user, session);
|
||||
|
||||
log.debugv("processAccessCode: isResource: {0}", isResource);
|
||||
log.debugv("processAccessCode: go to oauth page?: {0}",
|
||||
!isResource);
|
||||
|
||||
audit.detail(Details.CODE_ID, accessCode.getCodeId());
|
||||
|
||||
Set<RequiredAction> requiredActions = user.getRequiredActions();
|
||||
if (!requiredActions.isEmpty()) {
|
||||
RequiredAction action = user.getRequiredActions().iterator().next();
|
||||
accessCode.setRequiredAction(action);
|
||||
|
||||
if (action.equals(RequiredAction.VERIFY_EMAIL)) {
|
||||
audit.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
|
||||
}
|
||||
|
||||
return Flows.forms(this.session, realm, client, uriInfo).setAccessCode(accessCode.getCode()).setUser(user)
|
||||
.createResponse(action);
|
||||
}
|
||||
|
||||
if (!isResource) {
|
||||
accessCode.setAction(ClientSessionModel.Action.OAUTH_GRANT);
|
||||
|
||||
List<RoleModel> realmRoles = new LinkedList<RoleModel>();
|
||||
MultivaluedMap<String, RoleModel> resourceRoles = new MultivaluedMapImpl<String, RoleModel>();
|
||||
for (RoleModel r : accessCode.getRequestedRoles()) {
|
||||
if (r.getContainer() instanceof RealmModel) {
|
||||
realmRoles.add(r);
|
||||
} else {
|
||||
resourceRoles.add(((ApplicationModel) r.getContainer()).getName(), r);
|
||||
}
|
||||
}
|
||||
|
||||
return Flows.forms(this.session, realm, client, uriInfo)
|
||||
.setAccessCode(accessCode.getCode())
|
||||
.setAccessRequest(realmRoles, resourceRoles)
|
||||
.setClient(client)
|
||||
.createOAuthGrant();
|
||||
}
|
||||
|
||||
if (redirect != null) {
|
||||
audit.success();
|
||||
|
||||
accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN);
|
||||
return redirectAccessCode(accessCode, session, state, redirect);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Response forwardToSecurityFailure(String message) {
|
||||
return Flows.forms(session, realm, null, uriInfo).setError(message).createErrorPage();
|
||||
}
|
||||
|
||||
private void isTotpConfigurationRequired(UserModel user) {
|
||||
for (RequiredCredentialModel c : realm.getRequiredCredentials()) {
|
||||
if (c.getType().equals(CredentialRepresentation.TOTP) && !user.isTotp()) {
|
||||
user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
|
||||
log.debug("User is required to configure totp");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void isEmailVerificationRequired(UserModel user) {
|
||||
if (realm.isVerifyEmail() && !user.isEmailVerified()) {
|
||||
user.addRequiredAction(RequiredAction.VERIFY_EMAIL);
|
||||
log.debug("User is required to verify email");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package org.keycloak.services.util;
|
||||
|
||||
import org.jboss.resteasy.spi.HttpResponse;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -24,16 +24,12 @@ public class CookieHelper {
|
|||
* @param httpOnly
|
||||
*/
|
||||
public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) {
|
||||
HttpServletResponse response = ResteasyProviderFactory.getContextData(HttpServletResponse.class);
|
||||
Cookie cookie = new Cookie(name, value);
|
||||
if (path != null) cookie.setPath(path);
|
||||
if (domain != null) cookie.setDomain(domain);
|
||||
if (comment != null) cookie.setComment(comment);
|
||||
cookie.setMaxAge(maxAge);
|
||||
cookie.setSecure(secure);
|
||||
cookie.setHttpOnly(httpOnly);
|
||||
|
||||
response.addCookie(cookie);
|
||||
|
||||
HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class);
|
||||
StringBuffer cookieBuf = new StringBuffer();
|
||||
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly);
|
||||
String cookie = cookieBuf.toString();
|
||||
response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
305
services/src/main/java/org/keycloak/services/util/ServerCookie.java
Executable file
305
services/src/main/java/org/keycloak/services/util/ServerCookie.java
Executable file
|
@ -0,0 +1,305 @@
|
|||
package org.keycloak.services.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.DateFormat;
|
||||
import java.text.FieldPosition;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Server-side cookie representation. borrowed from Tomcat.
|
||||
*/
|
||||
public class ServerCookie implements Serializable {
|
||||
private static final String tspecials = ",; ";
|
||||
private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t";
|
||||
|
||||
/*
|
||||
* Tests a string and returns true if the string counts as a
|
||||
* reserved token in the Java language.
|
||||
*
|
||||
* @param value the <code>String</code> to be tested
|
||||
*
|
||||
* @return <code>true</code> if the <code>String</code> is a reserved
|
||||
* token; <code>false</code> if it is not
|
||||
*/
|
||||
public static boolean isToken(String value) {
|
||||
if (value == null) return true;
|
||||
int len = value.length();
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = value.charAt(i);
|
||||
|
||||
if (tspecials.indexOf(c) != -1)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean containsCTL(String value, int version) {
|
||||
if (value == null) return false;
|
||||
int len = value.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = value.charAt(i);
|
||||
if (c < 0x20 || c >= 0x7f) {
|
||||
if (c == 0x09)
|
||||
continue; //allow horizontal tabs
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static boolean isToken2(String value) {
|
||||
if (value == null) return true;
|
||||
int len = value.length();
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = value.charAt(i);
|
||||
if (tspecials2.indexOf(c) != -1)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated - Not used
|
||||
*/
|
||||
public static boolean checkName(String name) {
|
||||
if (!isToken(name)
|
||||
|| name.equalsIgnoreCase("Comment") // rfc2019
|
||||
|| name.equalsIgnoreCase("Discard") // rfc2965
|
||||
|| name.equalsIgnoreCase("Domain") // rfc2019
|
||||
|| name.equalsIgnoreCase("Expires") // Netscape
|
||||
|| name.equalsIgnoreCase("Max-Age") // rfc2019
|
||||
|| name.equalsIgnoreCase("Path") // rfc2019
|
||||
|| name.equalsIgnoreCase("Secure") // rfc2019
|
||||
|| name.equalsIgnoreCase("Version") // rfc2019
|
||||
// TODO remaining RFC2965 attributes
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------- Cookie parsing tools
|
||||
|
||||
|
||||
/**
|
||||
* Return the header name to set the cookie, based on cookie version.
|
||||
*/
|
||||
public static String getCookieHeaderName(int version) {
|
||||
// TODO Re-enable logging when RFC2965 is implemented
|
||||
// log( (version==1) ? "Set-Cookie2" : "Set-Cookie");
|
||||
if (version == 1) {
|
||||
// XXX RFC2965 not referenced in Servlet Spec
|
||||
// Set-Cookie2 is not supported by Netscape 4, 6, IE 3, 5
|
||||
// Set-Cookie2 is supported by Lynx and Opera
|
||||
// Need to check on later IE and FF releases but for now...
|
||||
// RFC2109
|
||||
return "Set-Cookie";
|
||||
// return "Set-Cookie2";
|
||||
} else {
|
||||
// Old Netscape
|
||||
return "Set-Cookie";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* US locale - all HTTP dates are in english
|
||||
*/
|
||||
private final static Locale LOCALE_US = Locale.US;
|
||||
|
||||
/**
|
||||
* GMT timezone - all HTTP dates are on GMT
|
||||
*/
|
||||
public final static TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT");
|
||||
/**
|
||||
* Pattern used for old cookies
|
||||
*/
|
||||
private final static String OLD_COOKIE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z";
|
||||
|
||||
|
||||
private final static DateFormat oldCookieFormat = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US);
|
||||
|
||||
public static String formatOldCookie(Date d) {
|
||||
String ocf = null;
|
||||
synchronized (oldCookieFormat) {
|
||||
ocf = oldCookieFormat.format(d);
|
||||
}
|
||||
return ocf;
|
||||
}
|
||||
|
||||
public static void formatOldCookie(Date d, StringBuffer sb,
|
||||
FieldPosition fp) {
|
||||
synchronized (oldCookieFormat) {
|
||||
oldCookieFormat.format(d, sb, fp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final String ancientDate = formatOldCookie(new Date(10000));
|
||||
|
||||
|
||||
// TODO RFC2965 fields also need to be passed
|
||||
public static void appendCookieValue(StringBuffer headerBuf,
|
||||
int version,
|
||||
String name,
|
||||
String value,
|
||||
String path,
|
||||
String domain,
|
||||
String comment,
|
||||
int maxAge,
|
||||
boolean isSecure,
|
||||
boolean httpOnly) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
// Servlet implementation checks name
|
||||
buf.append(name);
|
||||
buf.append("=");
|
||||
// Servlet implementation does not check anything else
|
||||
|
||||
// NOTE!!! BROWSERS REALLY DON'T LIKE QUOTING
|
||||
//maybeQuote2(version, buf, value);
|
||||
buf.append(value);
|
||||
|
||||
// Add version 1 specific information
|
||||
if (version == 1) {
|
||||
// Version=1 ... required
|
||||
buf.append("; Version=1");
|
||||
|
||||
// Comment=comment
|
||||
if (comment != null) {
|
||||
buf.append("; Comment=");
|
||||
//maybeQuote2(version, buf, comment);
|
||||
buf.append(comment);
|
||||
}
|
||||
}
|
||||
|
||||
// Add domain information, if present
|
||||
if (domain != null) {
|
||||
buf.append("; Domain=");
|
||||
//maybeQuote2(version, buf, domain);
|
||||
buf.append(domain);
|
||||
}
|
||||
|
||||
// Max-Age=secs ... or use old "Expires" format
|
||||
// TODO RFC2965 Discard
|
||||
if (maxAge >= 0) {
|
||||
// Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format )
|
||||
buf.append("; Expires=");
|
||||
// To expire immediately we need to set the time in past
|
||||
if (maxAge == 0)
|
||||
buf.append(ancientDate);
|
||||
else
|
||||
formatOldCookie
|
||||
(new Date(System.currentTimeMillis() +
|
||||
maxAge * 1000L), buf,
|
||||
new FieldPosition(0));
|
||||
|
||||
buf.append("; Max-Age=");
|
||||
buf.append(maxAge);
|
||||
}
|
||||
|
||||
// Path=path
|
||||
if (path != null) {
|
||||
buf.append("; Path=");
|
||||
buf.append(path);
|
||||
}
|
||||
|
||||
// Secure
|
||||
if (isSecure) {
|
||||
buf.append("; Secure");
|
||||
}
|
||||
|
||||
// HttpOnly
|
||||
if (httpOnly) {
|
||||
buf.append("; HttpOnly");
|
||||
}
|
||||
|
||||
headerBuf.append(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated - Not used
|
||||
*/
|
||||
@Deprecated
|
||||
public static void maybeQuote(int version, StringBuffer buf, String value) {
|
||||
// special case - a \n or \r shouldn't happen in any case
|
||||
if (isToken(value)) {
|
||||
buf.append(value);
|
||||
} else {
|
||||
buf.append('"');
|
||||
buf.append(escapeDoubleQuotes(value, 0, value.length()));
|
||||
buf.append('"');
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean alreadyQuoted(String value) {
|
||||
if (value == null || value.length() == 0) return false;
|
||||
return (value.charAt(0) == '\"' && value.charAt(value.length() - 1) == '\"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes values using rules that vary depending on Cookie version.
|
||||
*
|
||||
* @param version
|
||||
* @param buf
|
||||
* @param value
|
||||
*/
|
||||
public static void maybeQuote2(int version, StringBuffer buf, String value) {
|
||||
if (value == null || value.length() == 0) {
|
||||
buf.append("\"\"");
|
||||
} else if (containsCTL(value, version))
|
||||
throw new IllegalArgumentException("Control character in cookie value, consider BASE64 encoding your value");
|
||||
else if (alreadyQuoted(value)) {
|
||||
buf.append('"');
|
||||
buf.append(escapeDoubleQuotes(value, 1, value.length() - 1));
|
||||
buf.append('"');
|
||||
} else if (version == 0 && !isToken(value)) {
|
||||
buf.append('"');
|
||||
buf.append(escapeDoubleQuotes(value, 0, value.length()));
|
||||
buf.append('"');
|
||||
} else if (version == 1 && !isToken2(value)) {
|
||||
buf.append('"');
|
||||
buf.append(escapeDoubleQuotes(value, 0, value.length()));
|
||||
buf.append('"');
|
||||
} else {
|
||||
buf.append(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Escapes any double quotes in the given string.
|
||||
*
|
||||
* @param s the input string
|
||||
* @param beginIndex start index inclusive
|
||||
* @param endIndex exclusive
|
||||
* @return The (possibly) escaped string
|
||||
*/
|
||||
private static String escapeDoubleQuotes(String s, int beginIndex, int endIndex) {
|
||||
|
||||
if (s == null || s.length() == 0 || s.indexOf('"') == -1) {
|
||||
return s;
|
||||
}
|
||||
|
||||
StringBuffer b = new StringBuffer();
|
||||
for (int i = beginIndex; i < endIndex; i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c == '\\') {
|
||||
b.append(c);
|
||||
//ignore the character after an escape, just append it
|
||||
if (++i >= endIndex) throw new IllegalArgumentException("Invalid escape character in cookie value.");
|
||||
b.append(s.charAt(i));
|
||||
} else if (c == '"')
|
||||
b.append('\\').append('"');
|
||||
else
|
||||
b.append(c);
|
||||
}
|
||||
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@ package org.keycloak.testsuite.forms;
|
|||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.RuleChain;
|
||||
|
@ -102,6 +103,13 @@ public class FederationProvidersIntegrationTest {
|
|||
@WebResource
|
||||
protected AccountPasswordPage changePasswordPage;
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void runit() throws Exception {
|
||||
Thread.sleep(10000000);
|
||||
|
||||
}
|
||||
|
||||
static UserModel addUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
|
||||
UserModel user = session.users().addUser(realm, username);
|
||||
user.setEmail(email);
|
||||
|
|
|
@ -180,6 +180,11 @@ public class AccessTokenPerfTest {
|
|||
}
|
||||
Assert.assertEquals(302, response.getStatus());
|
||||
uri = response.getLocation();
|
||||
for (String header : response.getHeaders().keySet()) {
|
||||
for (Object value : response.getHeaders().get(header)) {
|
||||
System.out.println(header + ": " + value);
|
||||
}
|
||||
}
|
||||
response.close();
|
||||
|
||||
Assert.assertNotNull(uri);
|
||||
|
|
Loading…
Reference in a new issue