role mappers and testing
This commit is contained in:
parent
5cf64546c8
commit
28a5e61dff
4 changed files with 96 additions and 131 deletions
|
@ -30,6 +30,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.OAuthClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredCredentialModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
|
@ -372,85 +373,22 @@ public class OIDCLoginProtocolService {
|
|||
Map<String, String> err = new HashMap<String, String>();
|
||||
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Token invalid");
|
||||
logger.error("Invalid token. Token verification failed.");
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||
.build();
|
||||
}
|
||||
event.user(token.getSubject()).session(token.getSessionState()).detail(Details.VALIDATE_ACCESS_TOKEN, token.getId());
|
||||
|
||||
if (token.isExpired()
|
||||
|| token.getIssuedAt() < realm.getNotBefore()
|
||||
) {
|
||||
Map<String, String> err = new HashMap<String, String>();
|
||||
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Token expired");
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
UserModel user = session.users().getUserById(token.getSubject(), realm);
|
||||
if (user == null) {
|
||||
Map<String, String> err = new HashMap<String, String>();
|
||||
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||
err.put(OAuth2Constants.ERROR_DESCRIPTION, "User does not exist");
|
||||
event.error(Errors.USER_NOT_FOUND);
|
||||
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||
.build();
|
||||
}
|
||||
|
||||
if (!user.isEnabled()) {
|
||||
Map<String, String> err = new HashMap<String, String>();
|
||||
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||
err.put(OAuth2Constants.ERROR_DESCRIPTION, "User disabled");
|
||||
event.error(Errors.USER_DISABLED);
|
||||
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||
.build();
|
||||
}
|
||||
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
|
||||
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||
Map<String, String> err = new HashMap<String, String>();
|
||||
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Expired session");
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||
.build();
|
||||
}
|
||||
|
||||
ClientModel client = realm.findClient(token.getIssuedFor());
|
||||
if (client == null) {
|
||||
Map<String, String> err = new HashMap<String, String>();
|
||||
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_CLIENT);
|
||||
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Issued for client no longer exists");
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
if (token.getIssuedAt() < client.getNotBefore()) {
|
||||
Map<String, String> err = new HashMap<String, String>();
|
||||
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_CLIENT);
|
||||
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Issued for client no longer exists");
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
tokenManager.verifyAccess(token, realm, client, user);
|
||||
tokenManager.validateToken(session, uriInfo, clientConnection, realm, token);
|
||||
} catch (OAuthErrorException e) {
|
||||
Map<String, String> err = new HashMap<String, String>();
|
||||
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_SCOPE);
|
||||
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Role mappings have changed");
|
||||
Map<String, String> error = new HashMap<String, String>();
|
||||
error.put(OAuth2Constants.ERROR, e.getError());
|
||||
if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription());
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||
.build();
|
||||
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
|
||||
}
|
||||
|
||||
event.success();
|
||||
|
||||
return Response.ok(token, MediaType.APPLICATION_JSON_TYPE).build();
|
||||
|
@ -666,16 +604,6 @@ public class OIDCLoginProtocolService {
|
|||
|
||||
AccessToken token = tokenManager.createClientAccessToken(session, accessCode.getRequestedRoles(), realm, client, user, userSession, clientSession);
|
||||
|
||||
try {
|
||||
tokenManager.verifyAccess(token, realm, client, user);
|
||||
} catch (OAuthErrorException e) {
|
||||
Map<String, String> error = new HashMap<String, String>();
|
||||
error.put(OAuth2Constants.ERROR, e.getError());
|
||||
if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription());
|
||||
event.error(Errors.INVALID_CODE);
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
|
||||
}
|
||||
|
||||
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
|
||||
.accessToken(token)
|
||||
.generateIDToken()
|
||||
|
|
|
@ -59,12 +59,22 @@ public class TokenManager {
|
|||
}
|
||||
}
|
||||
|
||||
public AccessTokenResponse refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel client, String encodedRefreshToken, EventBuilder event) throws OAuthErrorException {
|
||||
RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken);
|
||||
public static class TokenValidation {
|
||||
public final UserModel user;
|
||||
public final UserSessionModel userSession;
|
||||
public final ClientSessionModel clientSession;
|
||||
public final AccessToken newToken;
|
||||
|
||||
event.user(refreshToken.getSubject()).session(refreshToken.getSessionState()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
|
||||
public TokenValidation(UserModel user, UserSessionModel userSession, ClientSessionModel clientSession, AccessToken newToken) {
|
||||
this.user = user;
|
||||
this.userSession = userSession;
|
||||
this.clientSession = clientSession;
|
||||
this.newToken = newToken;
|
||||
}
|
||||
}
|
||||
|
||||
UserModel user = session.users().getUserById(refreshToken.getSubject(), realm);
|
||||
public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, AccessToken oldToken) throws OAuthErrorException {
|
||||
UserModel user = session.users().getUserById(oldToken.getSubject(), realm);
|
||||
if (user == null) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user");
|
||||
}
|
||||
|
@ -73,24 +83,14 @@ public class TokenManager {
|
|||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
|
||||
}
|
||||
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, refreshToken.getSessionState());
|
||||
int currentTime = Time.currentTime();
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, oldToken.getSessionState());
|
||||
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||
AuthenticationManager.logout(session, realm, userSession, uriInfo, connection);
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
|
||||
}
|
||||
|
||||
if (!client.getClientId().equals(refreshToken.getIssuedFor())) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Unmatching clients", "Unmatching clients");
|
||||
}
|
||||
|
||||
if (refreshToken.getIssuedAt() < client.getNotBefore()) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
|
||||
}
|
||||
|
||||
ClientSessionModel clientSession = null;
|
||||
for (ClientSessionModel clientSessionModel : userSession.getClientSessions()) {
|
||||
if (clientSessionModel.getId().equals(refreshToken.getClientSession())) {
|
||||
if (clientSessionModel.getId().equals(oldToken.getClientSession())) {
|
||||
clientSession = clientSessionModel;
|
||||
break;
|
||||
}
|
||||
|
@ -98,20 +98,48 @@ public class TokenManager {
|
|||
|
||||
if (clientSession == null) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Client session not active", "Client session not active");
|
||||
|
||||
}
|
||||
|
||||
verifyAccess(refreshToken, realm, client, user);
|
||||
ClientModel client = clientSession.getClient();
|
||||
|
||||
AccessToken accessToken = initToken(realm, client, user, userSession, clientSession);
|
||||
accessToken.setRealmAccess(refreshToken.getRealmAccess());
|
||||
accessToken.setResourceAccess(refreshToken.getResourceAccess());
|
||||
accessToken = transformAccessToken(session, accessToken, realm, client, user, userSession, clientSession);
|
||||
if (!client.getClientId().equals(oldToken.getIssuedFor())) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Unmatching clients", "Unmatching clients");
|
||||
}
|
||||
|
||||
userSession.setLastSessionRefresh(currentTime);
|
||||
if (oldToken.getIssuedAt() < client.getNotBefore()) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
|
||||
}
|
||||
if (oldToken.getIssuedAt() < realm.getNotBefore()) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
|
||||
}
|
||||
|
||||
AccessTokenResponse res = responseBuilder(realm, client, event, session, userSession, clientSession)
|
||||
.accessToken(accessToken)
|
||||
|
||||
// recreate token.
|
||||
Set<RoleModel> requestedRoles = TokenManager.getAccess(null, clientSession.getClient(), user);
|
||||
AccessToken newToken = createClientAccessToken(session, requestedRoles, realm, client, user, userSession, clientSession);
|
||||
verifyAccess(oldToken, newToken);
|
||||
|
||||
return new TokenValidation(user, userSession, clientSession, newToken);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public AccessTokenResponse refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, String encodedRefreshToken, EventBuilder event) throws OAuthErrorException {
|
||||
RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken);
|
||||
|
||||
event.user(refreshToken.getSubject()).session(refreshToken.getSessionState()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
|
||||
|
||||
TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken);
|
||||
// validate authorizedClient is same as validated client
|
||||
if (!validation.clientSession.getClient().getId().equals(authorizedClient.getId())) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token. Token client and authorized client don't match");
|
||||
}
|
||||
|
||||
int currentTime = Time.currentTime();
|
||||
validation.userSession.setLastSessionRefresh(currentTime);
|
||||
|
||||
AccessTokenResponse res = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
|
||||
.accessToken(validation.newToken)
|
||||
.generateIDToken()
|
||||
.generateRefreshToken().build();
|
||||
return res;
|
||||
|
@ -198,41 +226,27 @@ public class TokenManager {
|
|||
return requestedRoles;
|
||||
}
|
||||
|
||||
public void verifyAccess(AccessToken token, RealmModel realm, ClientModel client, UserModel user) throws OAuthErrorException {
|
||||
ApplicationModel clientApp = (client instanceof ApplicationModel) ? (ApplicationModel)client : null;
|
||||
|
||||
|
||||
public void verifyAccess(AccessToken token, AccessToken newToken) throws OAuthErrorException {
|
||||
if (token.getRealmAccess() != null) {
|
||||
if (newToken.getRealmAccess() == null) throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for realm roles");
|
||||
|
||||
for (String roleName : token.getRealmAccess().getRoles()) {
|
||||
RoleModel role = realm.getRole(roleName);
|
||||
if (role == null) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid realm role " + roleName);
|
||||
}
|
||||
if (!user.hasRole(role)) {
|
||||
if (!newToken.getRealmAccess().getRoles().contains(roleName)) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for realm role: " + roleName);
|
||||
}
|
||||
if (!client.hasScope(role)) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "Client no longer has realm scope: " + roleName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (token.getResourceAccess() != null) {
|
||||
for (Map.Entry<String, AccessToken.Access> entry : token.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: " + entry.getKey());
|
||||
AccessToken.Access appAccess = newToken.getResourceAccess(entry.getKey());
|
||||
if (appAccess == null && !entry.getValue().getRoles().isEmpty()) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User or application no longer has role permissions for application key: " + entry.getKey());
|
||||
|
||||
}
|
||||
for (String roleName : entry.getValue().getRoles()) {
|
||||
RoleModel role = app.getRole(roleName);
|
||||
if (role == null) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown application role: " + roleName);
|
||||
}
|
||||
if (!user.hasRole(role)) {
|
||||
if (!appAccess.getRoles().contains(roleName)) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for application role " + roleName);
|
||||
}
|
||||
if (clientApp != null && !clientApp.equals(app) && !client.hasScope(role)) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "Client no longer has application scope" + roleName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -276,8 +276,8 @@ public class AccountTest {
|
|||
|
||||
events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent();
|
||||
|
||||
Assert.assertEquals("", profilePage.getFirstName());
|
||||
Assert.assertEquals("", profilePage.getLastName());
|
||||
Assert.assertEquals("Tom", profilePage.getFirstName());
|
||||
Assert.assertEquals("Brady", profilePage.getLastName());
|
||||
Assert.assertEquals("test-user@localhost", profilePage.getEmail());
|
||||
|
||||
// All fields are required, so there should be an error when something is missing.
|
||||
|
@ -310,8 +310,8 @@ public class AccountTest {
|
|||
|
||||
profilePage.clickCancel();
|
||||
|
||||
Assert.assertEquals("", profilePage.getFirstName());
|
||||
Assert.assertEquals("", profilePage.getLastName());
|
||||
Assert.assertEquals("Tom", profilePage.getFirstName());
|
||||
Assert.assertEquals("Brady", profilePage.getLastName());
|
||||
Assert.assertEquals("test-user@localhost", profilePage.getEmail());
|
||||
|
||||
events.assertEmpty();
|
||||
|
|
|
@ -653,6 +653,29 @@ public class AccessTokenTest {
|
|||
response.close();
|
||||
}
|
||||
client.close();
|
||||
|
||||
// undo mappers
|
||||
{
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
RealmModel realm = session.realms().getRealmByName("test");
|
||||
ApplicationModel app = realm.getApplicationByName("test-app");
|
||||
for (ProtocolMapperModel model : app.getProtocolMappers()) {
|
||||
if (model.getName().equals("address")
|
||||
|| model.getName().equals("hard")
|
||||
|| model.getName().equals("hard-nested")
|
||||
|| model.getName().equals("custom phone")
|
||||
|| model.getName().equals("nested phone")
|
||||
|| model.getName().equals("hard-realm")
|
||||
|| model.getName().equals("hard-app")
|
||||
) {
|
||||
app.removeProtocolMapper(model);
|
||||
}
|
||||
}
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
}
|
||||
|
||||
|
||||
events.clear();
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue