This commit is contained in:
Bill Burke 2013-10-17 13:53:46 -04:00
parent 6705819984
commit 2a6b6ebef5
11 changed files with 212 additions and 35 deletions

View file

@ -21,12 +21,14 @@ import java.util.concurrent.atomic.AtomicLong;
* @version $Revision: 1 $
*/
public class AbstractOAuthClient {
public static final String OAUTH_TOKEN_REQUEST_STATE = "OAuth_Token_Request_State";
protected String clientId;
protected String password;
protected KeyStore truststore;
protected String authUrl;
protected String codeUrl;
protected String stateCookieName = "OAuth_Token_Request_State";
protected String stateCookieName = OAUTH_TOKEN_REQUEST_STATE;
protected String stateCookiePath;
protected Client client;
protected boolean isSecure;
protected final AtomicLong counter = new AtomicLong();
@ -35,6 +37,9 @@ public class AbstractOAuthClient {
return counter.getAndIncrement() + "/" + UUID.randomUUID().toString();
}
/**
* Creates a Client for obtaining access token from code
*/
public void start() {
if (client == null) {
client = new ResteasyClientBuilder().trustStore(truststore)
@ -44,6 +49,9 @@ public class AbstractOAuthClient {
}
}
/**
* closes cllient
*/
public void stop() {
client.close();
}
@ -76,6 +84,8 @@ public class AbstractOAuthClient {
return authUrl;
}
public void setAuthUrl(String authUrl) {
this.authUrl = authUrl;
}
@ -96,6 +106,14 @@ public class AbstractOAuthClient {
this.stateCookieName = stateCookieName;
}
public String getStateCookiePath() {
return stateCookiePath;
}
public void setStateCookiePath(String stateCookiePath) {
this.stateCookiePath = stateCookiePath;
}
public Client getClient() {
return client;
}
@ -128,7 +146,6 @@ public class AbstractOAuthClient {
}
protected String stripOauthParametersFromRedirect(String uri) {
System.out.println("******************** redirect_uri: " + uri);
UriBuilder builder = UriBuilder.fromUri(uri)
.replaceQueryParam("code", null)
.replaceQueryParam("state", null);

View file

@ -1,5 +1,6 @@
package org.keycloak.jaxrs;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.AbstractOAuthClient;
import javax.ws.rs.BadRequestException;
@ -19,6 +20,7 @@ import java.net.URI;
* @version $Revision: 1 $
*/
public class JaxrsOAuthClient extends AbstractOAuthClient {
protected static final Logger logger = Logger.getLogger(JaxrsOAuthClient.class);
public Response redirect(UriInfo uriInfo, String redirectUri) {
String state = getStateCode();
@ -27,26 +29,42 @@ public class JaxrsOAuthClient extends AbstractOAuthClient {
.queryParam("redirect_uri", redirectUri)
.queryParam("state", state)
.build();
NewCookie cookie = new NewCookie(stateCookieName, state, uriInfo.getBaseUri().getPath(), null, null, -1, true);
NewCookie cookie = new NewCookie(getStateCookieName(), state, getStateCookiePath(uriInfo), null, null, -1, isSecure, true);
logger.info("NewCookie: " + cookie.toString());
return Response.status(302)
.location(url)
.cookie(cookie).build();
}
public String getBearerToken(UriInfo uriInfo, HttpHeaders headers) throws BadRequestException, InternalServerErrorException {
String error = uriInfo.getQueryParameters().getFirst("error");
if (error != null) throw new BadRequestException(new Exception("OAuth error: " + error));
Cookie stateCookie = headers.getCookies().get(stateCookieName);
if (stateCookie == null) throw new BadRequestException(new Exception("state cookie not set"));
;
public String getStateCookiePath(UriInfo uriInfo) {
if (stateCookiePath != null) return stateCookiePath;
return uriInfo.getBaseUri().getPath();
}
String state = uriInfo.getQueryParameters().getFirst("state");
if (state == null) throw new BadRequestException(new Exception("state parameter was null"));
if (!state.equals(stateCookie.getValue())) {
throw new BadRequestException(new Exception("state parameter invalid"));
}
String code = uriInfo.getQueryParameters().getFirst("code");
public String getBearerToken(UriInfo uriInfo, HttpHeaders headers) throws BadRequestException, InternalServerErrorException {
String error = getError(uriInfo);
if (error != null) throw new BadRequestException(new Exception("OAuth error: " + error));
checkStateCookie(uriInfo, headers);
String code = getAccessCode(uriInfo);
if (code == null) throw new BadRequestException(new Exception("code parameter was null"));
return resolveBearerToken(uriInfo.getRequestUri().toString(), code);
}
public String getError(UriInfo uriInfo) {
return uriInfo.getQueryParameters().getFirst("error");
}
public String getAccessCode(UriInfo uriInfo) {
return uriInfo.getQueryParameters().getFirst("code");
}
public void checkStateCookie(UriInfo uriInfo, HttpHeaders headers) {
Cookie stateCookie = headers.getCookies().get(stateCookieName);
if (stateCookie == null) throw new BadRequestException("state cookie not set");
String state = uriInfo.getQueryParameters().getFirst("state");
if (state == null) throw new BadRequestException("state parameter was null");
if (!state.equals(stateCookie.getValue())) {
throw new BadRequestException("state parameter invalid");
}
}
}

View file

@ -51,12 +51,13 @@ public class ServletOAuthClient extends AbstractOAuthClient {
.queryParam("redirect_uri", redirectUri)
.queryParam("state", state)
.build();
String cookiePath = request.getContextPath();
if (cookiePath.equals("")) cookiePath = "/";
String stateCookiePath = this.stateCookiePath;
if (stateCookiePath == null) stateCookiePath = request.getContextPath();
if (stateCookiePath.equals("")) stateCookiePath = "/";
Cookie cookie = new Cookie(stateCookieName, state);
cookie.setSecure(isSecure);
cookie.setPath(cookiePath);
cookie.setPath(stateCookiePath);
response.addCookie(cookie);
response.sendRedirect(url.toString());
}

12
forms/src/main/java/org/keycloak/forms/UrlBean.java Normal file → Executable file
View file

@ -70,19 +70,11 @@ public class UrlBean {
}
public String getLoginAction() {
if (realm.isSaas()) {
return Urls.saasLoginAction(baseURI).toString();
} else {
return Urls.realmLoginAction(baseURI, realm.getId()).toString();
}
return Urls.realmLoginAction(baseURI, realm.getId()).toString();
}
public String getLoginUrl() {
if (realm.isSaas()) {
return Urls.saasLoginPage(baseURI).toString();
} else {
return Urls.realmLoginPage(baseURI, realm.getId()).toString();
}
return Urls.realmLoginPage(baseURI, realm.getId()).toString();
}
public String getPasswordUrl() {

View file

@ -1,5 +1,6 @@
package org.keycloak.services.managers;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.SkeletonKeyToken;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
@ -23,6 +24,7 @@ public class AccessCodeEntry {
protected String redirectUri;
protected long expiration;
protected RealmModel realm;
protected SkeletonKeyToken token;
protected UserModel user;
protected Set<RequiredAction> requiredActions;
@ -38,6 +40,14 @@ public class AccessCodeEntry {
return id;
}
public RealmModel getRealm() {
return realm;
}
public void setRealm(RealmModel realm) {
this.realm = realm;
}
public String getCode() {
return code;
}

View file

@ -26,6 +26,7 @@ public class ApplianceBootstrap {
realm.addRequiredResourceCredential(CredentialRepresentation.PASSWORD);
realm.setTokenLifespan(300);
realm.setAccessCodeLifespan(60);
realm.setAccessCodeLifespanUserAction(300);
realm.setSslNotRequired(true);
realm.setCookieLoginAllowed(true);
realm.setRegistrationAllowed(false);
@ -49,7 +50,7 @@ public class ApplianceBootstrap {
password.setType(UserCredentialModel.PASSWORD);
password.setValue("admin");
realm.updateCredential(adminUser, password);
//adminUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
adminUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
adminConsole.grantRole(adminUser, adminRole);

View file

@ -99,7 +99,7 @@ public class AuthenticationManager {
expireCookie(SaasService.SAAS_IDENTITY_COOKIE, cookiePath);
}
protected void expireCookie(String cookieName, String path) {
public void expireCookie(String cookieName, String path) {
HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class);
if (response == null) {
logger.info("can't expire identity cookie, no HttpResponse");

View file

@ -3,6 +3,7 @@ package org.keycloak.services.managers;
import org.jboss.resteasy.jose.Base64Url;
import org.jboss.resteasy.jose.jws.JWSBuilder;
import org.jboss.resteasy.jwt.JsonSerialization;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.models.*;
import org.keycloak.representations.SkeletonKeyScope;
import org.keycloak.representations.SkeletonKeyToken;
@ -22,6 +23,7 @@ import java.util.concurrent.ConcurrentHashMap;
* @version $Revision: 1 $
*/
public class TokenManager {
protected static final Logger logger = Logger.getLogger(TokenManager.class);
protected Map<String, AccessCodeEntry> accessCodeMap = new ConcurrentHashMap<String, AccessCodeEntry>();
@ -86,6 +88,9 @@ public class TokenManager {
createToken(code, realm, client, user);
logger.info("tokenmanager: access code id: " + code.getId());
logger.info("accesscode setExpiration: " + (System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan());
code.setRealm(realm);
code.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan());
code.setClient(client);
code.setUser(user);

View file

@ -47,10 +47,9 @@ public class KeycloakApplication extends Application {
TokenManager tokenManager = new TokenManager();
singletons.add(new RealmsResource(tokenManager));
singletons.add(new SaasService(tokenManager));
singletons.add(new SocialResource(tokenManager, new SocialRequestManager()));
classes.add(SkeletonKeyContextResolver.class);
classes.add(SaasService.class);
}
protected KeycloakSessionFactory createSessionFactory() {

View file

@ -23,6 +23,7 @@ package org.keycloak.services.resources;
import org.jboss.resteasy.jose.jws.JWSInput;
import org.jboss.resteasy.jose.jws.crypto.RSAProvider;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
@ -52,6 +53,7 @@ import java.util.Set;
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RequiredActionsService {
protected static final Logger logger = Logger.getLogger(RequiredActionsService.class);
private RealmModel realm;
@ -134,10 +136,13 @@ public class RequiredActionsService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response updatePassword(final MultivaluedMap<String, String> formData) {
logger.info("updatePassword");
AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD);
if (accessCode == null) {
logger.info("updatePassword access code is null");
return forwardToErrorPage();
}
logger.info("updatePassword has access code");
UserModel user = getUser(accessCode);
@ -158,6 +163,8 @@ public class RequiredActionsService {
realm.updateCredential(user, credentials);
logger.info("updatePassword updated credential");
user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD);
if (accessCode != null) {
accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD);
@ -257,6 +264,7 @@ public class RequiredActionsService {
private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) {
String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE);
if (code == null) {
logger.info("getAccessCodeEntry code as not in query param");
return null;
}
@ -265,24 +273,31 @@ public class RequiredActionsService {
try {
verifiedCode = RSAProvider.verify(input, realm.getPublicKey());
} catch (Exception ignored) {
logger.info("getAccessCodeEntry code failed verification");
return null;
}
if (!verifiedCode) {
logger.info("getAccessCodeEntry code failed verification2");
return null;
}
String key = input.readContent(String.class);
AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key);
if (accessCodeEntry == null) {
logger.info("getAccessCodeEntry access code entry null");
return null;
}
if (accessCodeEntry.isExpired()) {
logger.info("getAccessCodeEntry: access code id: " + accessCodeEntry.getId());
logger.info("getAccessCodeEntry access code entry expired: " + accessCodeEntry.getExpiration());
logger.info("getAccessCodeEntry current time: " + (System.currentTimeMillis() / 1000));
return null;
}
if (accessCodeEntry.getRequiredActions() == null || !accessCodeEntry.getRequiredActions().contains(requiredAction)) {
logger.info("getAccessCodeEntry required actions null || entry does not contain required action: " + (accessCodeEntry.getRequiredActions() == null) + "|" + !accessCodeEntry.getRequiredActions().contains(requiredAction) );
return null;
}
@ -303,6 +318,7 @@ public class RequiredActionsService {
return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user)
.forwardToAction(requiredActions.iterator().next());
} else {
logger.info("redirectOauth: redirecting to: " + accessCode.getRedirectUri());
accessCode.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan());
return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode,
accessCode.getState(), accessCode.getRedirectUri());

View file

@ -1,21 +1,33 @@
package org.keycloak.services.resources;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.jose.jws.JWSInput;
import org.jboss.resteasy.jose.jws.crypto.RSAProvider;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.NotImplementedYetException;
import org.keycloak.AbstractOAuthClient;
import org.keycloak.jaxrs.JaxrsOAuthClient;
import org.keycloak.models.*;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.admin.RealmsAdminResource;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.OAuthFlows;
import javax.ws.rs.*;
import javax.ws.rs.container.ResourceContext;
import javax.ws.rs.core.*;
import javax.ws.rs.ext.Providers;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -42,8 +54,17 @@ public class SaasService {
@Context
protected ResourceContext resourceContext;
@Context
protected Providers providers;
protected String adminPath = "/admin/index.html";
protected AuthenticationManager authManager = new AuthenticationManager();
protected TokenManager tokenManager;
public SaasService(TokenManager tokenManager) {
this.tokenManager = tokenManager;
}
public static class WhoAmI {
protected String userId;
@ -168,7 +189,99 @@ public class SaasService {
RealmModel realm = getAdminstrationRealm(realmManager);
authManager.expireSaasIdentityCookie(uriInfo);
return Flows.forms(realm, request, uriInfo).forwardToLogin();
JaxrsOAuthClient oauth = new JaxrsOAuthClient();
String authUrl = TokenService.loginPageUrl(uriInfo).build(Constants.ADMIN_REALM).toString();
logger.info("authUrl: " + authUrl);
oauth.setAuthUrl(authUrl);
oauth.setClientId(Constants.ADMIN_CONSOLE_APPLICATION);
URI redirectUri = uriInfo.getBaseUriBuilder().path(SaasService.class).path(SaasService.class, "loginRedirect").build();
logger.info("redirectUri: " + redirectUri.toString());
oauth.setStateCookiePath(redirectUri.getPath());
return oauth.redirect(uriInfo, redirectUri.toString());
}
@Path("login-redirect")
@GET
@NoCache
public Response loginRedirect(@QueryParam("code") String code,
@QueryParam("state") String state,
@QueryParam("error") String error,
@Context HttpHeaders headers
) {
try {
logger.info("loginRedirect ********************** <---");
if (error != null) {
logger.debug("error from oauth");
throw new ForbiddenException("error");
}
RealmManager realmManager = new RealmManager(session);
RealmModel realm = getAdminstrationRealm(realmManager);
if (!realm.isEnabled()) {
logger.debug("realm not enabled");
throw new ForbiddenException();
}
ApplicationModel adminConsole = realm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION);
UserModel adminConsoleUser = adminConsole.getApplicationUser();
if (!adminConsole.isEnabled() || !adminConsoleUser.isEnabled()) {
logger.debug("admin app not enabled");
throw new ForbiddenException();
}
if (code == null) {
logger.debug("code not specified");
throw new BadRequestException();
}
if (state == null) {
logger.debug("state not specified");
throw new BadRequestException();
}
new JaxrsOAuthClient().checkStateCookie(uriInfo, headers);
JWSInput input = new JWSInput(code, providers);
boolean verifiedCode = false;
try {
verifiedCode = RSAProvider.verify(input, realm.getPublicKey());
} catch (Exception ignored) {
logger.debug("Failed to verify signature", ignored);
}
if (!verifiedCode) {
logger.debug("unverified access code");
throw new BadRequestException();
}
String key = input.readContent(String.class);
AccessCodeEntry accessCode = tokenManager.pullAccessCode(key);
if (accessCode == null) {
logger.debug("bad access code");
throw new BadRequestException();
}
if (accessCode.isExpired()) {
logger.debug("access code expired");
throw new BadRequestException();
}
if (!accessCode.getToken().isActive()) {
logger.debug("access token expired");
throw new BadRequestException();
}
if (!accessCode.getRealm().getId().equals(realm.getId())) {
logger.debug("bad realm");
throw new BadRequestException();
}
if (!adminConsoleUser.getLoginName().equals(accessCode.getClient().getLoginName())) {
logger.debug("bad client");
throw new BadRequestException();
}
if (!adminConsole.hasRole(accessCode.getUser(), Constants.ADMIN_CONSOLE_ADMIN_ROLE)) {
logger.debug("not allowed");
throw new ForbiddenException();
}
logger.info("loginRedirect SUCCESS");
NewCookie cookie = authManager.createSaasIdentityCookie(realm, accessCode.getUser(), uriInfo);
return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build();
} finally {
authManager.expireCookie(AbstractOAuthClient.OAUTH_TOKEN_REQUEST_STATE, uriInfo.getAbsolutePath().getPath());
}
}
@Path("logout")
@ -178,8 +291,9 @@ public class SaasService {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = getAdminstrationRealm(realmManager);
authManager.expireSaasIdentityCookie(uriInfo);
authManager.expireIdentityCookie(realm, uriInfo);
return Flows.forms(realm, request, uriInfo).forwardToLogin();
return Response.status(302).location(uriInfo.getBaseUriBuilder().path(SaasService.class).path(SaasService.class, "loginPage").build()).build();
}
@Path("logout-cookie")
@ -199,6 +313,8 @@ public class SaasService {
RealmModel realm = getAdminstrationRealm(realmManager);
if (realm == null)
throw new NotFoundException();
ApplicationModel adminConsole = realm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION);
UserModel adminConsoleUser = adminConsole.getApplicationUser();
if (!realm.isEnabled()) {
throw new NotImplementedYetException();
@ -208,6 +324,8 @@ public class SaasService {
AuthenticationStatus status = authManager.authenticateForm(realm, user, formData);
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
switch (status) {
case SUCCESS:
NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo);
@ -216,7 +334,7 @@ public class SaasService {
return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData)
.forwardToLogin();
case ACTIONS_REQUIRED:
return Flows.forms(realm, request, uriInfo).forwardToAction(user.getRequiredActions().iterator().next());
return oauth.processAccessCode(null, "n", contextRoot(uriInfo).path(adminPath).build().toString(), adminConsoleUser, user);
default:
return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData)
.forwardToLogin();