diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js index 3109d6df84..5cfc71010e 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js @@ -543,10 +543,16 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $scope.realm = realm; - $scope.realm.tokenLifespanUnit = TimeUnit.autoUnit(realm.tokenLifespan); - $scope.realm.tokenLifespan = TimeUnit.toUnit(realm.tokenLifespan, $scope.realm.tokenLifespanUnit); - $scope.$watch('realm.tokenLifespanUnit', function(to, from) { - $scope.realm.tokenLifespan = TimeUnit.convert($scope.realm.tokenLifespan, from, to); + $scope.realm.accessTokenLifespanUnit = TimeUnit.autoUnit(realm.accessTokenLifespan); + $scope.realm.accessTokenLifespan = TimeUnit.toUnit(realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit); + $scope.$watch('realm.accessTokenLifespanUnit', function(to, from) { + $scope.realm.accessTokenLifespan = TimeUnit.convert($scope.realm.accessTokenLifespan, from, to); + }); + + $scope.realm.refreshTokenLifespanUnit = TimeUnit.autoUnit(realm.refreshTokenLifespan); + $scope.realm.refreshTokenLifespan = TimeUnit.toUnit(realm.refreshTokenLifespan, $scope.realm.refreshTokenLifespanUnit); + $scope.$watch('realm.refreshTokenLifespanUnit', function(to, from) { + $scope.realm.refreshTokenLifespan = TimeUnit.convert($scope.realm.tokenLifespan, from, to); }); $scope.realm.accessCodeLifespanUnit = TimeUnit.autoUnit(realm.accessCodeLifespan); @@ -573,11 +579,13 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $scope.save = function() { var realmCopy = angular.copy($scope.realm); - delete realmCopy["tokenLifespanUnit"]; + delete realmCopy["accessTokenLifespanUnit"]; + delete realmCopy["refreshTokenLifespanUnit"]; delete realmCopy["accessCodeLifespanUnit"]; delete realmCopy["accessCodeLifespanUserActionUnit"]; - realmCopy.tokenLifespan = TimeUnit.toSeconds($scope.realm.tokenLifespan, $scope.realm.tokenLifespanUnit) + realmCopy.accessTokenLifespan = TimeUnit.toSeconds($scope.realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit) + realmCopy.refreshTokenLifespan = TimeUnit.toSeconds($scope.realm.refreshTokenLifespan, $scope.realm.refreshTokenLifespanUnit) realmCopy.accessCodeLifespan = TimeUnit.toSeconds($scope.realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit) realmCopy.accessCodeLifespanUserAction = TimeUnit.toSeconds($scope.realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit) diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-tokens.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-tokens.html index 74b9b362ae..0d1704efb5 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-tokens.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-tokens.html @@ -11,7 +11,7 @@
- +
@@ -66,6 +66,26 @@
+
+ +
+
+
+ +
+
+ +
+
+
+
diff --git a/core/src/main/java/org/keycloak/OAuthErrorException.java b/core/src/main/java/org/keycloak/OAuthErrorException.java new file mode 100755 index 0000000000..3cddba2990 --- /dev/null +++ b/core/src/main/java/org/keycloak/OAuthErrorException.java @@ -0,0 +1,62 @@ +package org.keycloak; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class OAuthErrorException extends Exception { + public static final String INVALID_REQUEST = "invalid_request"; + public static final String INVALID_CLIENT = "invalid_client"; + public static final String INVALID_GRANT = "invalid_grant"; + public static final String INVALID_SCOPE = "invalid_grant"; + public static final String UNAUTHORIZED_CLIENT = "unauthorized_client"; + public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type"; + + public OAuthErrorException(String error, String description, String message, Throwable cause) { + super(message, cause); + this.error = error; + this.description = description; + } + public OAuthErrorException(String error, String description, String message) { + super(message); + } + public OAuthErrorException(String error, String description) { + super(description); + this.error = error; + this.description = description; + } + public OAuthErrorException(String error, String description, Throwable cause) { + super(description, cause); + this.error = error; + this.description = description; + } + + public OAuthErrorException(String error) { + super(error); + this.error = error; + } + public OAuthErrorException(String error, Throwable cause) { + super(error, cause); + this.error = error; + } + + + protected String error; + protected String description; + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/core/src/main/java/org/keycloak/ServiceUrlConstants.java b/core/src/main/java/org/keycloak/ServiceUrlConstants.java index b1c9aa4ac0..f40d83e18b 100755 --- a/core/src/main/java/org/keycloak/ServiceUrlConstants.java +++ b/core/src/main/java/org/keycloak/ServiceUrlConstants.java @@ -8,4 +8,5 @@ public interface ServiceUrlConstants { public static final String TOKEN_SERVICE_LOGIN_PATH = "/rest/realms/{realm-name}/tokens/login"; public static final String TOKEN_SERVICE_ACCESS_CODE_PATH = "/rest/realms/{realm-name}/tokens/access/codes"; + public static final String TOKEN_SERVICE_REFRESH_PATH = "/rest/realms/{realm-name}/tokens/refresh"; } diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java index 8341c573fd..c99a509a3a 100755 --- a/core/src/main/java/org/keycloak/representations/AccessToken.java +++ b/core/src/main/java/org/keycloak/representations/AccessToken.java @@ -79,6 +79,13 @@ public class AccessToken extends JsonWebToken { return resourceAccess; } + public void setResourceAccess(Map resourceAccess) { + this.resourceAccess = resourceAccess; + } + + + + /** * Does the realm require verifying the caller? * @@ -130,6 +137,7 @@ public class AccessToken extends JsonWebToken { return (AccessToken) super.notBefore(notBefore); } + @Override public AccessToken issuedAt(long issuedAt) { return (AccessToken) super.issuedAt(issuedAt); diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index e4d70bb7ae..acdca83f20 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -14,6 +14,7 @@ public class RealmRepresentation { protected String id; protected String realm; protected Integer accessTokenLifespan; + protected Integer refreshTokenLifespan; protected Integer accessCodeLifespan; protected Integer accessCodeLifespanUserAction; protected Boolean enabled; @@ -122,6 +123,14 @@ public class RealmRepresentation { this.accessTokenLifespan = accessTokenLifespan; } + public Integer getRefreshTokenLifespan() { + return refreshTokenLifespan; + } + + public void setRefreshTokenLifespan(Integer refreshTokenLifespan) { + this.refreshTokenLifespan = refreshTokenLifespan; + } + public List getRoleMappings() { return roleMappings; } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/TokenGrantRequest.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/TokenGrantRequest.java index 4f3d2ffff6..029356d408 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/TokenGrantRequest.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/TokenGrantRequest.java @@ -46,21 +46,18 @@ public class TokenGrantRequest { } } - public static AccessTokenResponse invoke(RealmConfiguration config, String code, String redirectUri) throws HttpFailure, IOException { + public static AccessTokenResponse invokeAccessCodeToToken(RealmConfiguration config, String code, String redirectUri) throws HttpFailure, IOException { String codeUrl = config.getCodeUrl(); String client_id = config.getMetadata().getResourceName(); Map credentials = config.getResourceCredentials(); HttpClient client = config.getClient(); - return invoke(client, code, codeUrl, redirectUri, client_id, credentials); + return invokeAccessCodeToToken(client, code, codeUrl, redirectUri, client_id, credentials); } - public static AccessTokenResponse invoke(HttpClient client, String code, String codeUrl, String redirectUri, String client_id, Map credentials) throws IOException, HttpFailure { + public static AccessTokenResponse invokeAccessCodeToToken(HttpClient client, String code, String codeUrl, String redirectUri, String client_id, Map credentials) throws IOException, HttpFailure { List formparams = new ArrayList(); redirectUri = stripOauthParametersFromRedirect(redirectUri); - for (Map.Entry entry : credentials.entrySet()) { - formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); - } formparams.add(new BasicNameValuePair("grant_type", "authorization_code")); formparams.add(new BasicNameValuePair("code", code)); formparams.add(new BasicNameValuePair("redirect_uri", redirectUri)); @@ -106,6 +103,64 @@ public class TokenGrantRequest { } } + public static AccessTokenResponse invokeRefresh(RealmConfiguration config, String refreshToken) throws IOException, HttpFailure { + String refreshUrl = config.getRefreshUrl(); + String client_id = config.getMetadata().getResourceName(); + Map credentials = config.getResourceCredentials(); + HttpClient client = config.getClient(); + return invokeRefresh(client, refreshToken, refreshUrl, client_id, credentials); + } + + + public static AccessTokenResponse invokeRefresh(HttpClient client, String refreshToken, String refreshUrl, String client_id, Map credentials) throws IOException, HttpFailure { + List formparams = new ArrayList(); + for (Map.Entry entry : credentials.entrySet()) { + formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); + } + formparams.add(new BasicNameValuePair("grant_type", "refresh_token")); + formparams.add(new BasicNameValuePair("refresh_token", refreshToken)); + HttpResponse response = null; + UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); + HttpPost post = new HttpPost(refreshUrl); + String clientSecret = credentials.get(CredentialRepresentation.SECRET); + if (clientSecret != null) { + String authorization = BasicAuthHelper.createHeader(client_id, clientSecret); + post.setHeader("Authorization", authorization); + } + + post.setEntity(form); + response = client.execute(post); + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + if (status != 200) { + error(status, entity); + } + if (entity == null) { + throw new HttpFailure(status, null); + } + InputStream is = entity.getContent(); + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + int c; + while ((c = is.read()) != -1) { + os.write(c); + } + byte[] bytes = os.toByteArray(); + String json = new String(bytes); + try { + return JsonSerialization.readValue(json, AccessTokenResponse.class); + } catch (IOException e) { + throw new IOException(json, e); + } + } finally { + try { + is.close(); + } catch (IOException ignored) { + + } + } + } + protected static void error(int status, HttpEntity entity) throws HttpFailure, IOException { String body = null; diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/config/RealmConfiguration.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/config/RealmConfiguration.java index fe4cf3afc6..18537f2875 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/config/RealmConfiguration.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/config/RealmConfiguration.java @@ -16,6 +16,7 @@ public class RealmConfiguration { protected HttpClient client; protected KeycloakUriBuilder authUrl; protected String codeUrl; + protected String refreshUrl; protected Map resourceCredentials = new HashMap(); protected boolean sslRequired = true; protected String stateCookieName = "OAuth_Token_Request_State"; @@ -72,6 +73,14 @@ public class RealmConfiguration { this.codeUrl = codeUrl; } + public String getRefreshUrl() { + return refreshUrl; + } + + public void setRefreshUrl(String refreshUrl) { + this.refreshUrl = refreshUrl; + } + public Map getResourceCredentials() { return resourceCredentials; } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/config/RealmConfigurationLoader.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/config/RealmConfigurationLoader.java index 264fb6bd4e..a1b1e2c3c8 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/config/RealmConfigurationLoader.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/config/RealmConfigurationLoader.java @@ -37,6 +37,7 @@ public class RealmConfigurationLoader extends AdapterConfigLoader { KeycloakUriBuilder serverBuilder = KeycloakUriBuilder.fromUri(adapterConfig.getAuthServerUrl()); String authUrl = serverBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_LOGIN_PATH).build(adapterConfig.getRealm()).toString(); String tokenUrl = serverBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_ACCESS_CODE_PATH).build(adapterConfig.getRealm()).toString(); + String refreshUrl = serverBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_REFRESH_PATH).build(adapterConfig.getRealm()).toString(); realmConfiguration.setMetadata(resourceMetadata); realmConfiguration.setSslRequired(!adapterConfig.isSslNotRequired()); @@ -47,6 +48,7 @@ public class RealmConfigurationLoader extends AdapterConfigLoader { realmConfiguration.setClient(client); realmConfiguration.setAuthUrl(KeycloakUriBuilder.fromUri(authUrl).queryParam("client_id", resourceMetadata.getResourceName())); realmConfiguration.setCodeUrl(tokenUrl); + realmConfiguration.setRefreshUrl(refreshUrl); } protected void initClient() { diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java index a2b3c30858..2f9ca0742b 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java @@ -225,7 +225,7 @@ public class ServletOAuthLogin { String redirectUri = stripOauthParametersFromRedirect(); AccessTokenResponse tokenResponse = null; try { - tokenResponse = TokenGrantRequest.invoke(realmInfo, code, redirectUri); + tokenResponse = TokenGrantRequest.invokeAccessCodeToToken(realmInfo, code, redirectUri); } catch (TokenGrantRequest.HttpFailure failure) { log.error("failed to turn code into token"); log.error("status from server: " + failure.getStatus()); diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java index 4290651840..5d17c0a654 100755 --- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java @@ -11,8 +11,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URI; -import java.util.HashMap; -import java.util.Map; /** * @author Bill Burke @@ -48,7 +46,7 @@ public class ServletOAuthClient extends AbstractOAuthClient { } public String resolveBearerToken(String redirectUri, String code) throws IOException, TokenGrantRequest.HttpFailure { - return TokenGrantRequest.invoke(client, code, codeUrl, redirectUri, clientId, credentials).getToken(); + return TokenGrantRequest.invokeAccessCodeToToken(client, code, codeUrl, redirectUri, clientId, credentials).getToken(); } /** diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OAuthAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OAuthAuthenticator.java index 6205e171bf..654e974a77 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OAuthAuthenticator.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OAuthAuthenticator.java @@ -234,7 +234,7 @@ public class OAuthAuthenticator { AccessTokenResponse tokenResponse = null; String redirectUri = stripOauthParametersFromRedirect(); try { - tokenResponse = TokenGrantRequest.invoke(realmInfo, code, redirectUri); + tokenResponse = TokenGrantRequest.invokeAccessCodeToToken(realmInfo, code, redirectUri); } catch (TokenGrantRequest.HttpFailure failure) { log.error("failed to turn code into token"); log.error("status from server: " + failure.getStatus()); diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index a0aad3785a..b5682eef90 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -185,4 +185,6 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa * @return */ UserCredentialModel getSecret(UserModel user); + + boolean hasScope(UserModel user, RoleModel role); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 608788e566..204e1d7062 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -892,6 +892,19 @@ public class RealmAdapter implements RealmModel { return false; } + @Override + public boolean hasScope(UserModel user, RoleModel role) { + Set roles = getScopeMappings(user); + if (roles.contains(role)) return true; + + for (RoleModel mapping : roles) { + if (mapping.hasRole(role)) return true; + } + return false; + } + + + protected TypedQuery getUserRoleMappingEntityTypedQuery(UserAdapter user, RoleAdapter role) { TypedQuery query = em.createNamedQuery("userHasRole", UserRoleMappingEntity.class); query.setParameter("user", ((UserAdapter)user).getUser()); @@ -996,12 +1009,6 @@ public class RealmAdapter implements RealmModel { } } - public boolean hasScope(UserModel user, RoleModel role) { - TypedQuery query = getRealmScopeMappingQuery((UserAdapter) user, (RoleAdapter) role); - return query.getResultList().size() > 0; - } - - protected TypedQuery getRealmScopeMappingQuery(UserAdapter user, RoleAdapter role) { TypedQuery query = em.createNamedQuery("userHasScope", UserScopeMappingEntity.class); query.setParameter("user", ((UserAdapter)user).getUser()); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 06991cc70d..be880ad569 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -615,6 +615,18 @@ public class RealmAdapter extends AbstractAdapter implements RealmModel { return realmRoles; } + @Override + public boolean hasScope(UserModel user, RoleModel role) { + Set roles = getScopeMappings(user); + if (roles.contains(role)) return true; + + for (RoleModel mapping : roles) { + if (mapping.hasRole(role)) return true; + } + return false; + } + + @Override public void addScopeMapping(UserModel agent, RoleModel role) { UserEntity userEntity = ((UserAdapter)agent).getUser(); 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 74b5bd406c..75204ba601 100755 --- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java +++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java @@ -1,7 +1,10 @@ package org.keycloak.services.managers; import org.jboss.resteasy.logging.Logger; +import org.keycloak.OAuthErrorException; import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.ApplicationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -9,6 +12,8 @@ import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.AccessScope; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.RefreshToken; import org.keycloak.util.Base64Url; import org.keycloak.util.JsonSerialization; @@ -16,6 +21,7 @@ import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.security.PrivateKey; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -109,6 +115,75 @@ public class TokenManager { return code; } + public AccessToken refreshAccessToken(RealmModel realm, UserModel client, String encodedRefreshToken) throws OAuthErrorException { + JWSInput jws = new JWSInput(encodedRefreshToken); + RefreshToken refreshToken = null; + try { + if (!RSAProvider.verify(jws, realm.getPublicKey())) { + throw new RuntimeException("Invalid refresh token"); + } + refreshToken = jws.readJsonContent(RefreshToken.class); + } catch (IOException e) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e); + } + if (refreshToken.isExpired()) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired"); + } + + UserModel user = realm.getUserById(refreshToken.getSubject()); + if (user == null) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user"); + } + + if (!user.isEnabled()) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled"); + + } + + ApplicationModel clientApp = realm.getApplicationByName(client.getLoginName()); + + + if (refreshToken.getRealmAccess() != null) { + for (String roleName : refreshToken.getRealmAccess().getRoles()) { + RoleModel role = realm.getRole(roleName); + if (role == null) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid realm role " + roleName); + } + if (!realm.hasRole(user, role)) { + throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for realm role: " + roleName); + } + if (!realm.hasScope(client, role)) { + throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "Client no longer has realm scope: " + roleName); + } + } + } + if (refreshToken.getResourceAccess() != null) { + for (Map.Entry entry : refreshToken.getResourceAccess().entrySet()) { + ApplicationModel app = realm.getApplicationByName(entry.getKey()); + if (app == null) { + throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "Application no longer exists", "Application no longer exists: " + app.getName()); + } + for (String roleName : refreshToken.getRealmAccess().getRoles()) { + RoleModel role = app.getRole(roleName); + if (role == null) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown application role: " + roleName); + } + if (!realm.hasRole(user, role)) { + throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for application role " + roleName); + } + if (clientApp != null && !clientApp.equals(app) && !realm.hasScope(client, role)) { + throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "Client no longer has application scope" + roleName); + } + } + + } + } + AccessToken accessToken = initToken(realm, client, user); + accessToken.setRealmAccess(refreshToken.getRealmAccess()); + accessToken.setResourceAccess(refreshToken.getResourceAccess()); + return accessToken; + } + public AccessToken createClientAccessToken(String scopeParam, RealmModel realm, UserModel client, UserModel user) { return createClientAccessToken(scopeParam, realm, client, user, new LinkedList(), new MultivaluedHashMap()); } @@ -172,6 +247,7 @@ public class TokenManager { token.audience(realm.getName()); token.issuedNow(); token.issuedFor(client.getLoginName()); + token.issuer(realm.getName()); if (realm.getAccessTokenLifespan() > 0) { token.expiration((System.currentTimeMillis() / 1000) + realm.getAccessTokenLifespan()); } @@ -232,26 +308,68 @@ public class TokenManager { } - public AccessToken createAccessToken(RealmModel realm, UserModel user) { - AccessToken token = new AccessToken(); - token.id(KeycloakModelUtils.generateId()); - token.issuedNow(); - token.subject(user.getId()); - token.audience(realm.getName()); - if (realm.getAccessTokenLifespan() > 0) { - token.expiration((System.currentTimeMillis() / 1000) + realm.getAccessTokenLifespan()); - } - for (RoleModel role : realm.getRoleMappings(user)) { - addComposites(token, role); - } - return token; - } - - public String encodeToken(RealmModel realm, Object token) { String encodedToken = new JWSBuilder() .jsonContent(token) .rsa256(realm.getPrivateKey()); return encodedToken; } + + public AccessTokenResponseBuilder responseBuilder(RealmModel realm) { + return new AccessTokenResponseBuilder(realm); + } + + public class AccessTokenResponseBuilder { + RealmModel realm; + AccessToken accessToken; + RefreshToken refreshToken; + + public AccessTokenResponseBuilder(RealmModel realm) { + this.realm = realm; + } + + public AccessTokenResponseBuilder accessToken(AccessToken accessToken) { + this.accessToken = accessToken; + return this; + } + public AccessTokenResponseBuilder refreshToken(RefreshToken refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + public AccessTokenResponseBuilder generateAccessToken(String scopeParam, UserModel client, UserModel user) { + accessToken = createClientAccessToken(scopeParam, realm, client, user); + return this; + } + + public AccessTokenResponseBuilder generateRefreshToken() { + if (accessToken == null) { + throw new IllegalStateException("accessToken not set"); + } + refreshToken = new RefreshToken(accessToken); + refreshToken.id(KeycloakModelUtils.generateId()); + refreshToken.issuedNow(); + refreshToken.expiration((System.currentTimeMillis() / 1000) + realm.getRefreshTokenLifespan()); + return this; + } + + public AccessTokenResponse build() { + AccessTokenResponse res = new AccessTokenResponse(); + if (accessToken != null) { + String encodedToken = new JWSBuilder().jsonContent(accessToken).rsa256(realm.getPrivateKey()); + res.setToken(encodedToken); + res.setTokenType("bearer"); + if (accessToken.getExpiration() != 0) { + long time = accessToken.getExpiration() - (System.currentTimeMillis() / 1000); + res.setExpiresIn(time); + } + } + if (refreshToken != null) { + String encodedToken = new JWSBuilder().jsonContent(refreshToken).rsa256(realm.getPrivateKey()); + res.setRefreshToken(encodedToken); + } + return res; + } + } + } 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 f38c985174..bc005f74f6 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -4,6 +4,7 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; +import org.keycloak.OAuthErrorException; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; @@ -156,9 +157,36 @@ public class TokenService { throw new NotAuthorizedException("Auth failed"); } String scope = form.getFirst("scope"); - AccessToken token = tokenManager.createClientAccessToken(scope, realm, client, user); - String encoded = tokenManager.encodeToken(realm, token); - AccessTokenResponse res = accessTokenResponse(token, encoded); + AccessTokenResponse res = tokenManager.responseBuilder(realm) + .generateAccessToken(scope, client, user).build(); + return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build(); + } + + @Path("refresh") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.APPLICATION_JSON) + public Response refreshAccessToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, + final MultivaluedMap form) { + if (!checkSsl()) { + throw new NotAcceptableException("HTTPS required"); + } + + UserModel client = authorizeClient(authorizationHeader); + String refreshToken = form.getFirst("refresh_token"); + AccessToken accessToken = null; + try { + accessToken = tokenManager.refreshAccessToken(realm, client, refreshToken); + } catch (OAuthErrorException e) { + Map error = new HashMap(); + error.put("error", e.getError()); + if (e.getDescription() != null) error.put("error_description", e.getDescription()); + throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(), e); + } + + AccessTokenResponse res = tokenManager.responseBuilder(realm) + .accessToken(accessToken) + .generateRefreshToken().build(); return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build(); } @@ -368,7 +396,9 @@ public class TokenService { .build(); } logger.debug("accessRequest SUCCESS"); - AccessTokenResponse res = accessTokenResponse(realm.getPrivateKey(), accessCode.getToken()); + AccessTokenResponse res = tokenManager.responseBuilder(realm) + .accessToken(accessCode.getToken()) + .generateRefreshToken().build(); return Cors.add(request, Response.ok(res)).allowedOrigins(client).allowedMethods("POST").build(); } @@ -408,23 +438,6 @@ public class TokenService { return client; } - protected AccessTokenResponse accessTokenResponse(PrivateKey privateKey, AccessToken token) { - String encodedToken = new JWSBuilder().jsonContent(token).rsa256(privateKey); - - return accessTokenResponse(token, encodedToken); - } - - protected AccessTokenResponse accessTokenResponse(AccessToken token, String encodedToken) { - AccessTokenResponse res = new AccessTokenResponse(); - res.setToken(encodedToken); - res.setTokenType("bearer"); - if (token.getExpiration() != 0) { - long time = token.getExpiration() - (System.currentTimeMillis() / 1000); - res.setExpiresIn(time); - } - return res; - } - @Path("login") @GET public Response loginPage(final @QueryParam("response_type") String responseType,