KEYCLOAK-9563 Improve access token checks for userinfo endpoint

This commit is contained in:
Martin Kanis 2020-01-15 08:59:50 +01:00 committed by Stian Thorgersen
parent 3ef338d392
commit 1d54f2ade3
3 changed files with 146 additions and 38 deletions

View file

@ -22,8 +22,10 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenCategory;
import org.keycloak.TokenVerifier;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.HashProvider;
import org.keycloak.crypto.SignatureProvider;
@ -56,6 +58,7 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
@ -161,16 +164,13 @@ public class TokenManager {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Unmatching clients", "Unmatching clients");
}
if (oldToken.getIssuedAt() < client.getNotBefore()) {
try {
TokenVerifier.createWithoutSignature(oldToken)
.withChecks(NotBeforeCheck.forModel(client), NotBeforeCheck.forModel(session, realm, user))
.verify();
} catch (VerificationException e) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
}
if (oldToken.getIssuedAt() < realm.getNotBefore()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
}
if (oldToken.getIssuedAt() < session.users().getNotBeforeOfUser(realm, user)) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
}
// Setup clientScopes from refresh token to the context
String oldTokenScope = oldToken.getScope();
@ -207,16 +207,16 @@ public class TokenManager {
* @throws OAuthErrorException
*/
public boolean checkTokenValidForIntrospection(KeycloakSession session, RealmModel realm, AccessToken token) throws OAuthErrorException {
if (!token.isActive()) {
return false;
}
if (token.getIssuedAt() < realm.getNotBefore()) {
return false;
}
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
if (client == null || !client.isEnabled() || token.getIssuedAt() < client.getNotBefore()) {
if (client == null || !client.isEnabled()) {
return false;
}
try {
TokenVerifier.createWithoutSignature(token)
.withChecks(NotBeforeCheck.forModel(client), TokenVerifier.IS_ACTIVE)
.verify();
} catch (VerificationException e) {
return false;
}
@ -248,9 +248,14 @@ public class TokenManager {
if (!user.isEnabled()) {
return false;
}
if (token.getIssuedAt() < session.users().getNotBeforeOfUser(realm, user)) {
try {
TokenVerifier.createWithoutSignature(token)
.withChecks(NotBeforeCheck.forModel(session ,realm, user))
.verify();
} catch (VerificationException e) {
return false;
}
if (token.getIssuedAt() + 1 < userSession.getStarted()) {
return false;
}
@ -349,12 +354,12 @@ public class TokenManager {
}
if (checkExpiration) {
if (refreshToken.getExpiration() != 0 && refreshToken.isExpired()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
}
if (refreshToken.getIssuedAt() < realm.getNotBefore()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
try {
TokenVerifier.createWithoutSignature(refreshToken)
.withChecks(NotBeforeCheck.forModel(realm), TokenVerifier.IS_ACTIVE)
.verify();
} catch (VerificationException e) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, e.getMessage());
}
}
@ -385,14 +390,12 @@ public class TokenManager {
public IDToken verifyIDToken(KeycloakSession session, RealmModel realm, String encodedIDToken) throws OAuthErrorException {
IDToken idToken = session.tokens().decode(encodedIDToken, IDToken.class);
if (idToken == null) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken");
}
if (idToken.isExpired()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "IDToken expired");
}
if (idToken.getIssuedAt() < realm.getNotBefore()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale IDToken");
try {
TokenVerifier.createWithoutSignature(idToken)
.withChecks(NotBeforeCheck.forModel(realm), TokenVerifier.IS_ACTIVE)
.verify();
} catch (VerificationException e) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, e.getMessage());
}
return idToken;
}
@ -881,4 +884,44 @@ public class TokenManager {
}
}
public static class NotBeforeCheck implements TokenVerifier.Predicate<JsonWebToken> {
private final int notBefore;
public NotBeforeCheck(int notBefore) {
this.notBefore = notBefore;
}
@Override
public boolean test(JsonWebToken t) throws VerificationException {
if (t.getIssuedAt() < notBefore) {
throw new VerificationException("Stale token");
}
return true;
}
public static NotBeforeCheck forModel(ClientModel clientModel) {
if (clientModel != null) {
int notBeforeClient = clientModel.getNotBefore();
int notBeforeRealm = clientModel.getRealm().getNotBefore();
int notBefore = (notBeforeClient == 0 ? notBeforeRealm : (notBeforeRealm == 0 ? notBeforeClient :
Math.min(notBeforeClient, notBeforeRealm)));
return new NotBeforeCheck(notBefore);
}
return new NotBeforeCheck(0);
}
public static NotBeforeCheck forModel(RealmModel realmModel) {
return new NotBeforeCheck(realmModel == null ? 0 : realmModel.getNotBefore());
}
public static NotBeforeCheck forModel(KeycloakSession session, RealmModel realmModel, UserModel userModel) {
return new NotBeforeCheck(session.users().getNotBeforeOfUser(realmModel, userModel));
}
}
}

View file

@ -35,6 +35,7 @@ import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.protocol.oidc.TokenManager.NotBeforeCheck;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -137,6 +138,7 @@ public class UserInfoEndpoint {
}
AccessToken token;
ClientModel clientModel;
try {
TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class).withDefaultChecks()
.realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
@ -145,17 +147,21 @@ public class UserInfoEndpoint {
verifier.verifierContext(verifierContext);
token = verifier.verify().getToken();
clientModel = realm.getClientByClientId(token.getIssuedFor());
if (clientModel == null) {
event.error(Errors.CLIENT_NOT_FOUND);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client not found", Response.Status.BAD_REQUEST);
}
TokenVerifier.createWithoutSignature(token)
.withChecks(NotBeforeCheck.forModel(clientModel))
.verify();
} catch (VerificationException e) {
event.error(Errors.INVALID_TOKEN);
throw newUnauthorizedErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Token verification failed");
}
ClientModel clientModel = realm.getClientByClientId(token.getIssuedFor());
if (clientModel == null) {
event.error(Errors.CLIENT_NOT_FOUND);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client not found", Response.Status.BAD_REQUEST);
}
if (!clientModel.getProtocol().equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException(Errors.INVALID_CLIENT, "Wrong client protocol.", Response.Status.BAD_REQUEST);

View file

@ -401,6 +401,65 @@ public class UserInfoTest extends AbstractKeycloakTest {
}
}
@Test
public void testNotBeforeTokens() {
Client client = ClientBuilder.newClient();
try {
AccessTokenResponse accessTokenResponse = executeGrantAccessTokenRequest(client);
int time = Time.currentTime() + 60;
RealmResource realm = adminClient.realm("test");
RealmRepresentation rep = realm.toRepresentation();
rep.setNotBefore(time);
realm.update(rep);
Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getToken());
assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
response.close();
events.expect(EventType.USER_INFO_REQUEST_ERROR)
.error(Errors.INVALID_TOKEN)
.user(Matchers.nullValue(String.class))
.session(Matchers.nullValue(String.class))
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
.client((String) null)
.assertEvent();
events.clear();
rep.setNotBefore(0);
realm.update(rep);
// do the same with client's notBefore
ClientResource clientResource = realm.clients().get(realm.clients().findByClientId("test-app").get(0).getId());
ClientRepresentation clientRep = clientResource.toRepresentation();
clientRep.setNotBefore(time);
clientResource.update(clientRep);
response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getToken());
assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
response.close();
events.expect(EventType.USER_INFO_REQUEST_ERROR)
.error(Errors.INVALID_TOKEN)
.user(Matchers.nullValue(String.class))
.session(Matchers.nullValue(String.class))
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
.client((String) null)
.assertEvent();
clientRep.setNotBefore(0);
clientResource.update(clientRep);
} finally {
client.close();
}
}
@Test
public void testSessionExpiredOfflineAccess() throws Exception {
Client client = ClientBuilder.newClient();