From 579aefd31041c289c71688831e32520a70a89f1f Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Sun, 28 Jul 2013 22:34:54 -0400 Subject: [PATCH] oauth --- .../services/managers/AccessCodeEntry.java | 33 +++ .../services/managers/TokenManager.java | 128 ++++++----- .../services/resources/TokenService.java | 202 ++++++++---------- 3 files changed, 186 insertions(+), 177 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java index d96b32ba95..63032d7a1d 100755 --- a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java +++ b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java @@ -1,8 +1,13 @@ package org.keycloak.services.managers; import org.keycloak.representations.SkeletonKeyToken; +import org.picketlink.idm.model.Role; import org.picketlink.idm.model.User; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; /** @@ -11,9 +16,13 @@ import java.util.UUID; */ public class AccessCodeEntry { protected String id = UUID.randomUUID().toString() + System.currentTimeMillis(); + protected String code; protected long expiration; protected SkeletonKeyToken token; + protected User user; protected User client; + protected List realmRolesRequested = new ArrayList(); + MultivaluedMap resourceRolesRequested = new MultivaluedHashMap(); public boolean isExpired() { return expiration != 0 && (System.currentTimeMillis() / 1000) > expiration; @@ -23,6 +32,14 @@ public class AccessCodeEntry { return id; } + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + public long getExpiration() { return expiration; } @@ -46,4 +63,20 @@ public class AccessCodeEntry { public void setClient(User client) { this.client = client; } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public List getRealmRolesRequested() { + return realmRolesRequested; + } + + public MultivaluedMap getResourceRolesRequested() { + return resourceRolesRequested; + } } diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java index 68ef55007b..1c4bb00353 100755 --- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java +++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java @@ -3,19 +3,16 @@ 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.spi.HttpResponse; -import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.representations.SkeletonKeyScope; import org.keycloak.representations.SkeletonKeyToken; import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.ResourceModel; import org.keycloak.services.resources.RealmsResource; +import org.picketlink.idm.model.Role; import org.picketlink.idm.model.User; -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.NewCookie; -import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -40,6 +37,9 @@ public class TokenManager { accessCodeMap.clear(); } + public AccessCodeEntry getAccessCode(String key) { + return accessCodeMap.get(key); + } public AccessCodeEntry pullAccessCode(String key) { return accessCodeMap.remove(key); @@ -54,16 +54,57 @@ public class TokenManager { return cookie; } - public String createAccessCode(String scopeParam, RealmModel realm, User client, User user) - { - SkeletonKeyToken token = null; - if (scopeParam != null) token = createScopedToken(scopeParam, realm, client, user); - else token = createUnscopedToken(realm, client, user); - + public AccessCodeEntry createAccessCode(String scopeParam, RealmModel realm, User client, User user) { AccessCodeEntry code = new AccessCodeEntry(); + SkeletonKeyScope scopeMap = null; + if (scopeParam != null) scopeMap = decodeScope(scopeParam); + List realmRolesRequested = code.getRealmRolesRequested(); + MultivaluedMap resourceRolesRequested = code.getResourceRolesRequested(); + Set realmMapping = realm.getRoleMappings(user); + + if (realmMapping != null && realmMapping.size() > 0 && (scopeMap == null || scopeMap.containsKey("realm"))) { + Set scope = realm.getScope(client); + if (scope.size() > 0) { + Set scopeRequest = null; + if (scopeMap != null) { + scopeRequest.addAll(scopeMap.get("realm")); + if (scopeRequest.contains(RealmManager.WILDCARD_ROLE)) scopeRequest = null; + } + for (String role : realmMapping) { + if ( + (scopeRequest == null || scopeRequest.contains(role)) && + (scope.contains("*") || scope.contains(role)) + ) + realmRolesRequested.add(realm.getIdm().getRole(role)); + } + } + } + for (ResourceModel resource : realm.getResources()) { + Set mapping = resource.getRoleMappings(user); + if (mapping != null && mapping.size() > 0 && (scopeMap == null || scopeMap.containsKey(resource.getName()))) { + Set scope = resource.getScope(client); + if (scope.size() > 0) { + Set scopeRequest = null; + if (scopeMap != null) { + scopeRequest.addAll(scopeMap.get(resource.getName())); + if (scopeRequest.contains(RealmManager.WILDCARD_ROLE)) scopeRequest = null; + } + for (String role : mapping) { + if ( + (scopeRequest == null || scopeRequest.contains(role)) && + (scope.contains("*") || scope.contains(role)) + ) + resourceRolesRequested.add(resource.getName(), resource.getIdm().getRole(role)); + } + } + } + } + + + createToken(code, realm, client, user); code.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan()); - code.setToken(token); code.setClient(client); + code.setUser(user); accessCodeMap.put(code.getId(), code); String accessCode = null; try { @@ -71,30 +112,8 @@ public class TokenManager { } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } - return accessCode; - } - - public SkeletonKeyToken createScopedToken(SkeletonKeyScope scope, RealmModel realm, User client, User user) { - SkeletonKeyToken token = initToken(realm, client, user); - Map resourceMap = realm.getResourceMap(); - - for (String res : scope.keySet()) { - ResourceModel resource = resourceMap.get(res); - Set scopeMapping = resource.getScope(client); - Set roleMapping = resource.getRoleMappings(user); - SkeletonKeyToken.Access access = token.addAccess(resource.getName()); - for (String role : scope.get(res)) { - if (!scopeMapping.contains("*") && !scopeMapping.contains(role)) { - throw new ForbiddenException(Response.status(403).entity("

Security Alert

Known client not authorized for the requested scope.

").type("text/html").build()); - } - if (!roleMapping.contains(role)) { - throw new ForbiddenException(Response.status(403).entity("

Security Alert

Known client not authorized for the requested scope.

").type("text/html").build()); - - } - access.addRole(role); - } - } - return token; + code.setCode(accessCode); + return code; } protected SkeletonKeyToken initToken(RealmModel realm, User client, User user) { @@ -110,38 +129,29 @@ public class TokenManager { return token; } - public SkeletonKeyToken createScopedToken(String scopeParam, RealmModel realm, User client, User user) { - SkeletonKeyScope scope = decodeScope(scopeParam); - return createScopedToken(scope, realm, client, user); - } - - public SkeletonKeyToken createUnscopedToken(RealmModel realm, User client, User user) { + protected void createToken(AccessCodeEntry accessCodeEntry, RealmModel realm, User client, User user) { SkeletonKeyToken token = initToken(realm, client, user); - Set realmMapping = realm.getRoleMappings(user); - - if (realmMapping != null && realmMapping.size() > 0) { - Set scope = realm.getScope(client); + if (accessCodeEntry.getRealmRolesRequested().size() > 0) { SkeletonKeyToken.Access access = new SkeletonKeyToken.Access(); - for (String role : realmMapping) { - if (scope.contains("*") || scope.contains(role)) access.addRole(role); + for (Role role : accessCodeEntry.getRealmRolesRequested()) { + access.addRole(role.getName()); } token.setRealmAccess(access); } - List resources = realm.getResources(); - for (ResourceModel resource : resources) { - Set scope = resource.getScope(client); - Set mapping = resource.getRoleMappings(user); - if (mapping.size() == 0 || scope.size() == 0) continue; - SkeletonKeyToken.Access access = token.addAccess(resource.getName()) - .verifyCaller(resource.isSurrogateAuthRequired()); - for (String role : mapping) { - if (scope.contains("*") || scope.contains(role)) access.addRole(role); + + if (accessCodeEntry.getResourceRolesRequested().size() > 0) { + Map resourceMap = realm.getResourceMap(); + for (String resourceName : accessCodeEntry.getResourceRolesRequested().keySet()) { + ResourceModel resource = resourceMap.get(resourceName); + SkeletonKeyToken.Access access = token.addAccess(resourceName).verifyCaller(resource.isSurrogateAuthRequired()); + for (Role role : accessCodeEntry.getResourceRolesRequested().get(resourceName)) { + access.addRole(role.getName()); + } } } - return token; - + accessCodeEntry.setToken(token); } public String encodeScope(SkeletonKeyScope scope) { diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index eb52cfe39f..f450491ec9 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -1,12 +1,13 @@ package org.keycloak.services.resources; -import org.jboss.resteasy.jose.Base64Url; import org.jboss.resteasy.jose.jws.JWSBuilder; import org.jboss.resteasy.jose.jws.JWSInput; import org.jboss.resteasy.jose.jws.crypto.RSAProvider; import org.jboss.resteasy.jwt.JsonSerialization; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; +import org.jboss.resteasy.spi.HttpResponse; +import org.keycloak.TokenIdGenerator; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.SkeletonKeyScope; import org.keycloak.representations.SkeletonKeyToken; @@ -17,7 +18,6 @@ import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.models.RealmModel; -import org.keycloak.services.models.RequiredCredentialModel; import org.keycloak.services.models.ResourceModel; import org.picketlink.idm.IdentitySession; import org.picketlink.idm.model.Role; @@ -31,15 +31,20 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; +import java.net.URI; import java.security.PrivateKey; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -66,10 +71,13 @@ public class TokenService { protected IdentitySession identitySession; @Context HttpRequest request; + @Context + HttpResponse response; protected String securityFailurePath = "/securityFailure.jsp"; protected String loginFormPath = "/loginForm.jsp"; + protected String oauthFormPath = "/oauthForm.jsp"; protected RealmModel realm; protected TokenManager tokenManager; @@ -110,6 +118,10 @@ public class TokenService { return tokenServiceBaseUrl(uriInfo).path(TokenService.class, "processLogin"); } + public static UriBuilder processOAuthUrl(UriInfo uriInfo) { + return tokenServiceBaseUrl(uriInfo).path(TokenService.class, "processOAuth"); + } + @Path("grants/identity-token") @POST @@ -211,16 +223,35 @@ public class TokenService { return null; } - return redirectAccessCode(scopeParam, state, redirect, client, user); + return processAccessCode(scopeParam, state, redirect, client, user); } - protected Response redirectAccessCode(String scopeParam, String state, String redirect, User client, User user) { - String accessCode = tokenManager.createAccessCode(scopeParam, realm, client, user); - UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", accessCode); + protected Response processAccessCode(String scopeParam, String state, String redirect, User client, User user) { + Role resourceRole = realm.getIdm().getRole(RealmManager.RESOURCE_ROLE); + Role oauthClientRole = realm.getIdm().getRole(RealmManager.OAUTH_CLIENT_ROLE); + boolean isResource = realm.getIdm().hasRole(client, resourceRole); + if (!isResource && !realm.getIdm().hasRole(client, oauthClientRole)) { + securityFailureForward("Login requester not allowed to request login."); + identitySession.close(); + return null; + } + AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, realm, client, user); + + if (!isResource && accessCode.getRealmRolesRequested().size() > 0 && accessCode.getResourceRolesRequested().size() > 0) { + oauthGrantPage(accessCode, client, state, redirect); + identitySession.close(); + return null; + } + return redirectAccessCode(accessCode, state, redirect); + } + + protected Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect) { + String code = accessCode.getCode(); + UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", code); if (state != null) redirectUri.queryParam("state", state); Response.ResponseBuilder location = Response.status(302).location(redirectUri.build()); if (realm.isCookieLoginAllowed()) { - location.cookie(tokenManager.createLoginCookie(realm, user, uriInfo)); + location.cookie(tokenManager.createLoginCookie(realm, accessCode.getUser(), uriInfo)); } return location.build(); } @@ -393,16 +424,16 @@ public class TokenService { } Role resourceRole = realm.getIdm().getRole(RealmManager.RESOURCE_ROLE); Role oauthClientRole = realm.getIdm().getRole(RealmManager.OAUTH_CLIENT_ROLE); - if (!realm.getIdm().hasRole(client, resourceRole) && !realm.getIdm().hasRole(client, oauthClientRole)) { + boolean isResource = realm.getIdm().hasRole(client, resourceRole); + if (!isResource && !realm.getIdm().hasRole(client, oauthClientRole)) { securityFailureForward("Login requester not allowed to request login."); identitySession.close(); return null; - } User user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); if (user != null) { - return redirectAccessCode(scopeParam, state, redirect, client, user); + return processAccessCode(scopeParam, state, redirect, client, user); } forwardToLoginForm(redirect, clientId, scopeParam, state); @@ -424,117 +455,52 @@ public class TokenService { return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build(); } - private Response loginForm(String validationError, String redirect, String clientId, String scopeParam, String state, RealmModel realm, User client) { - StringBuffer html = new StringBuffer(); - if (scopeParam != null) { - html.append("

Grant Request For ").append(realm.getName()).append(" Realm

"); - if (validationError != null) { - try { - Thread.sleep(1000); // put in a delay - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - html.append("

").append(validationError).append("

"); - } - html.append("

A Third Party is requesting access to the following resources

"); - html.append(""); - SkeletonKeyScope scope = tokenManager.decodeScope(scopeParam); - Map resourceMap = realm.getResourceMap(); - - for (String res : scope.keySet()) { - ResourceModel resource = resourceMap.get(res); - html.append(""); - } - html.append("
Resource: ").append(resource.getName()).append("Roles:"); - Set scopeMapping = resource.getScope(client); - for (String role : scope.get(res)) { - html.append(" ").append(role); - if (!scopeMapping.contains("*") && !scopeMapping.contains(role)) { - return Response.ok("

Security Alert

Known client not authorized for the requested scope.

").type("text/html").build(); - } - } - html.append("

To Authorize, please login below

"); - } else { - Set scopeMapping = realm.getScope(client); - if (scopeMapping.contains("*")) { - html.append("

Login For ").append(realm.getName()).append(" Realm

"); - if (validationError != null) { - try { - Thread.sleep(1000); // put in a delay - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - html.append("

").append(validationError).append("

"); - } - } else { - html.append("

Grant Request For ").append(realm.getName()).append(" Realm

"); - if (validationError != null) { - try { - Thread.sleep(1000); // put in a delay - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - html.append("

").append(validationError).append("

"); - } - SkeletonKeyScope scope = new SkeletonKeyScope(); - List resources = realm.getResources(); - boolean found = false; - for (ResourceModel resource : resources) { - Set resourceScope = resource.getScope(client); - if (resourceScope == null) continue; - if (resourceScope.size() == 0) continue; - if (!found) { - found = true; - html.append("

A Third Party is requesting access to the following resources

"); - html.append(""); - } - html.append("
Resource: ").append(resource.getName()).append("Roles:"); - // todo add description of role - for (String role : resourceScope) { - html.append(" ").append(role); - scope.add(resource.getName(), role); - } - } - if (!found) { - return Response.ok("

Security Alert

Known client not authorized to access this realm.

").type("text/html").build(); - } - html.append("
"); - try { - String json = JsonSerialization.toString(scope, false); - scopeParam = Base64Url.encode(json.getBytes("UTF-8")); - } catch (Exception e) { - throw new RuntimeException(e); - } - - } + @Path("oauth/grant") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response processOAuth(MultivaluedMap formData) { + String redirect = formData.getFirst("redirect_uri"); + String state = formData.getFirst("state"); + if (formData.containsKey("cancel")) { + return redirectAccessDenied(redirect, state); } + String code = formData.getFirst("code"); - UriBuilder formActionUri = processLoginUrl(uriInfo); - String action = formActionUri.build(realm.getId()).toString(); - html.append("
"); - html.append("Username:
"); - - for (RequiredCredentialModel credential : realm.getRequiredCredentials()) { - if (!credential.isInput()) continue; - html.append(credential.getType()).append(": "); - if (credential.isSecret()) { - html.append("
"); - - } else { - html.append("
"); - } + 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); } - html.append(""); - if (scopeParam != null) { - html.append(""); + if (!verifiedCode) { + return redirectAccessDenied(redirect, state); } - if (state != null) html.append(""); - html.append(""); - html.append(""); - html.append("
"); - return Response.ok(html.toString()).type("text/html").build(); + String key = input.readContent(String.class); + AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key); + if (accessCodeEntry == null) { + return redirectAccessDenied(redirect, state); + } + return redirectAccessCode(accessCodeEntry, state, redirect); } + + protected Response redirectAccessDenied(String redirect, String state) { + UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("error", "access_denied"); + if (state != null) redirectUri.queryParam("state", state); + Response.ResponseBuilder location = Response.status(302).location(redirectUri.build()); + return location.build(); + } + + protected void oauthGrantPage(AccessCodeEntry accessCode, User client, String state, String redirect_uri) { + request.setAttribute("realmRolesRequested", accessCode.getRealmRolesRequested()); + request.setAttribute("resourceRolesRequested", accessCode.getResourceRolesRequested()); + request.setAttribute("state", state); + request.setAttribute("redirect_uri", redirect_uri); + request.setAttribute("client", client); + request.setAttribute("action", processOAuthUrl(uriInfo)); + request.setAttribute("accessCode", accessCode.getCode()); + + request.forward(oauthFormPath); + } + }