KEYCLOAK-9563 Improve access token checks for userinfo endpoint
This commit is contained in:
parent
3ef338d392
commit
1d54f2ade3
3 changed files with 146 additions and 38 deletions
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue