Skip to content

Commit

Permalink
KEYCLOAK-4627 Changes in TokenVerifier to include token in exceptions…
Browse files Browse the repository at this point in the history
…. Reset credentials uses checks to validate individual token aspects
  • Loading branch information
hmlnarik authored and mposolda committed May 11, 2017
1 parent a9ec69e commit b55b089
Show file tree
Hide file tree
Showing 12 changed files with 571 additions and 282 deletions.
2 changes: 1 addition & 1 deletion core/src/main/java/org/keycloak/RSATokenVerifier.java
Expand Up @@ -32,7 +32,7 @@ public class RSATokenVerifier {
private final TokenVerifier<AccessToken> tokenVerifier; private final TokenVerifier<AccessToken> tokenVerifier;


private RSATokenVerifier(String tokenString) { private RSATokenVerifier(String tokenString) {
this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class); this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class).withDefaultChecks();
} }


public static RSATokenVerifier create(String tokenString) { public static RSATokenVerifier create(String tokenString) {
Expand Down
160 changes: 128 additions & 32 deletions core/src/main/java/org/keycloak/TokenVerifier.java
Expand Up @@ -33,16 +33,24 @@
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.*; import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;


/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class TokenVerifier<T extends JsonWebToken> { public class TokenVerifier<T extends JsonWebToken> {


private static final Logger LOG = Logger.getLogger(TokenVerifier.class.getName());

// This interface is here as JDK 7 is a requirement for this project. // This interface is here as JDK 7 is a requirement for this project.
// Once JDK 8 would become mandatory, java.util.function.Predicate would be used instead. // Once JDK 8 would become mandatory, java.util.function.Predicate would be used instead.


/**
* Functional interface of checks that verify some part of a JWT.
* @param <T> Type of the token handled by this predicate.
*/
// @FunctionalInterface // @FunctionalInterface
public static interface Predicate<T extends JsonWebToken> { public static interface Predicate<T extends JsonWebToken> {
/** /**
Expand All @@ -66,11 +74,15 @@ public boolean test(JsonWebToken t) throws VerificationException {
} }
}; };


/**
* Check for token being neither expired nor used before it gets valid.
* @see JsonWebToken#isActive()
*/
public static final Predicate<JsonWebToken> IS_ACTIVE = new Predicate<JsonWebToken>() { public static final Predicate<JsonWebToken> IS_ACTIVE = new Predicate<JsonWebToken>() {
@Override @Override
public boolean test(JsonWebToken t) throws VerificationException { public boolean test(JsonWebToken t) throws VerificationException {
if (! t.isActive()) { if (! t.isActive()) {
throw new TokenNotActiveException("Token is not active"); throw new TokenNotActiveException(t, "Token is not active");
} }


return true; return true;
Expand Down Expand Up @@ -143,29 +155,45 @@ protected TokenVerifier(T token) {
} }


/** /**
* Creates a {@code TokenVerifier<AccessToken> instance. The method is here for backwards compatibility. * Creates an instance of {@code TokenVerifier} from the given string on a JWT of the given class.
* @param tokenString * The token verifier has no checks defined. Note that the checks are only tested when
* {@link #verify()} method is invoked.
* @param <T> Type of the token
* @param tokenString String representation of JWT
* @param clazz Class of the token
* @return * @return
* @deprecated use {@link #create(java.lang.String, java.lang.Class) } instead
*/ */
public static TokenVerifier<AccessToken> create(String tokenString) { public static <T extends JsonWebToken> TokenVerifier<T> create(String tokenString, Class<T> clazz) {
return create(tokenString, AccessToken.class); return new TokenVerifier(tokenString, clazz);
} }


public static <T extends JsonWebToken> TokenVerifier<T> create(String tokenString, Class<T> clazz) { /**
return new TokenVerifier(tokenString, clazz) * Creates an instance of {@code TokenVerifier} from the given string on a JWT of the given class.
.check(RealmUrlCheck.NULL_INSTANCE) * The token verifier has no checks defined. Note that the checks are only tested when
.check(SUBJECT_EXISTS_CHECK) * {@link #verify()} method is invoked.
.check(TokenTypeCheck.INSTANCE_BEARER) * @return
.check(IS_ACTIVE); */
public static <T extends JsonWebToken> TokenVerifier<T> create(T token) {
return new TokenVerifier(token);
} }


public static <T extends JsonWebToken> TokenVerifier<T> from(T token) { /**
return new TokenVerifier(token) * Adds default checks to the token verification:
.check(RealmUrlCheck.NULL_INSTANCE) * <ul>
.check(SUBJECT_EXISTS_CHECK) * <li>Realm URL (JWT issuer field: {@code iss}) has to be defined and match realm set via {@link #realmUrl(java.lang.String)} method</li>
.check(TokenTypeCheck.INSTANCE_BEARER) * <li>Subject (JWT subject field: {@code sub}) has to be defined</li>
.check(IS_ACTIVE); * <li>Token type (JWT type field: {@code typ}) has to be {@code Bearer}. The type can be set via {@link #tokenType(java.lang.String)} method</li>
* <li>Token has to be active, ie. both not expired and not used before its validity (JWT issuer fields: {@code exp} and {@code nbf})</li>
* </ul>
* @return This token verifier.
*/
public TokenVerifier<T> withDefaultChecks() {
return withChecks(
RealmUrlCheck.NULL_INSTANCE,
SUBJECT_EXISTS_CHECK,
TokenTypeCheck.INSTANCE_BEARER,
IS_ACTIVE
);
} }


private void removeCheck(Class<? extends Predicate<?>> checkClass) { private void removeCheck(Class<? extends Predicate<?>> checkClass) {
Expand Down Expand Up @@ -197,59 +225,76 @@ private <P extends Predicate<? super T>> TokenVerifier<T> replaceCheck(Predicate
} }


/** /**
* Resets all preset checks and will test the given checks in {@link #verify()} method. * Will test the given checks in {@link #verify()} method in addition to already set checks.
* @param checks * @param checks
* @return * @return
*/ */
public TokenVerifier<T> checkOnly(Predicate<? super T>... checks) { public TokenVerifier<T> withChecks(Predicate<? super T>... checks) {
this.checks.clear();
if (checks != null) { if (checks != null) {
this.checks.addAll(Arrays.asList(checks)); this.checks.addAll(Arrays.asList(checks));
} }
return this; return this;
} }


/** /**
* Will test the given checks in {@link #verify()} method in addition to already set checks. * Sets the key for verification of RSA-based signature.
* @param checks * @param publicKey
* @return * @return
*/ */
public TokenVerifier<T> check(Predicate<? super T>... checks) {
if (checks != null) {
this.checks.addAll(Arrays.asList(checks));
}
return this;
}

public TokenVerifier<T> publicKey(PublicKey publicKey) { public TokenVerifier<T> publicKey(PublicKey publicKey) {
this.publicKey = publicKey; this.publicKey = publicKey;
return this; return this;
} }


/**
* Sets the key for verification of HMAC-based signature.
* @param secretKey
* @return
*/
public TokenVerifier<T> secretKey(SecretKey secretKey) { public TokenVerifier<T> secretKey(SecretKey secretKey) {
this.secretKey = secretKey; this.secretKey = secretKey;
return this; return this;
} }


/**
* @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
* @return This token verifier
*/
public TokenVerifier<T> realmUrl(String realmUrl) { public TokenVerifier<T> realmUrl(String realmUrl) {
this.realmUrl = realmUrl; this.realmUrl = realmUrl;
return replaceCheck(RealmUrlCheck.class, checkRealmUrl, new RealmUrlCheck(realmUrl)); return replaceCheck(RealmUrlCheck.class, checkRealmUrl, new RealmUrlCheck(realmUrl));
} }


/**
* @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
* @return This token verifier
*/
public TokenVerifier<T> checkTokenType(boolean checkTokenType) { public TokenVerifier<T> checkTokenType(boolean checkTokenType) {
this.checkTokenType = checkTokenType; this.checkTokenType = checkTokenType;
return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType)); return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
} }


/**
* @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
* @return This token verifier
*/
public TokenVerifier<T> tokenType(String tokenType) { public TokenVerifier<T> tokenType(String tokenType) {
this.expectedTokenType = tokenType; this.expectedTokenType = tokenType;
return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType)); return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
} }


/**
* @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
* @return This token verifier
*/
public TokenVerifier<T> checkActive(boolean checkActive) { public TokenVerifier<T> checkActive(boolean checkActive) {
return replaceCheck(IS_ACTIVE, checkActive, IS_ACTIVE); return replaceCheck(IS_ACTIVE, checkActive, IS_ACTIVE);
} }


/**
* @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
* @return This token verifier
*/
public TokenVerifier<T> checkRealmUrl(boolean checkRealmUrl) { public TokenVerifier<T> checkRealmUrl(boolean checkRealmUrl) {
this.checkRealmUrl = checkRealmUrl; this.checkRealmUrl = checkRealmUrl;
return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl)); return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl));
Expand Down Expand Up @@ -300,14 +345,14 @@ public void verifySignature() throws VerificationException {
throw new VerificationException("Public key not set"); throw new VerificationException("Public key not set");
} }
if (!RSAProvider.verify(jws, publicKey)) { if (!RSAProvider.verify(jws, publicKey)) {
throw new TokenSignatureInvalidException("Invalid token signature"); throw new TokenSignatureInvalidException(token, "Invalid token signature");
} break; } break;
case HMAC: case HMAC:
if (secretKey == null) { if (secretKey == null) {
throw new VerificationException("Secret key not set"); throw new VerificationException("Secret key not set");
} }
if (!HMACProvider.verify(jws, secretKey)) { if (!HMACProvider.verify(jws, secretKey)) {
throw new TokenSignatureInvalidException("Invalid token signature"); throw new TokenSignatureInvalidException(token, "Invalid token signature");
} break; } break;
default: default:
throw new VerificationException("Unknown or unsupported token algorithm"); throw new VerificationException("Unknown or unsupported token algorithm");
Expand All @@ -331,4 +376,55 @@ public TokenVerifier<T> verify() throws VerificationException {
return this; return this;
} }


/**
* Creates an optional predicate from a predicate that will proceed with check but always pass.
* @param <T>
* @param mandatoryPredicate
* @return
*/
public static <T extends JsonWebToken> Predicate<T> optional(final Predicate<T> mandatoryPredicate) {
return new Predicate<T>() {
@Override
public boolean test(T t) throws VerificationException {
try {
if (! mandatoryPredicate.test(t)) {
LOG.finer("[optional] predicate failed: " + mandatoryPredicate);
}

return true;
} catch (VerificationException ex) {
LOG.log(Level.FINER, "[optional] predicate " + mandatoryPredicate + " failed.", ex);
return true;
}
}
};
}

/**
* Creates a predicate that will proceed with checks of the given predicates
* and will pass if and only if at least one of the given predicates passes.
* @param <T>
* @param predicates
* @return
*/
public static <T extends JsonWebToken> Predicate<T> alternative(final Predicate<? super T>... predicates) {
return new Predicate<T>() {
@Override
public boolean test(T t) throws VerificationException {
for (Predicate<? super T> predicate : predicates) {
try {
if (predicate.test(t)) {
return true;
}

LOG.finer("[alternative] predicate failed: " + predicate);
} catch (VerificationException ex) {
LOG.log(Level.FINER, "[alternative] predicate " + predicate + " failed.", ex);
}
}

return false;
}
};
}
} }
Expand Up @@ -16,28 +16,29 @@
*/ */
package org.keycloak.exceptions; package org.keycloak.exceptions;


import org.keycloak.common.VerificationException; import org.keycloak.representations.JsonWebToken;


/** /**
* Exception thrown for cases when token is invalid due to time constraints (expired, or not yet valid). * Exception thrown for cases when token is invalid due to time constraints (expired, or not yet valid).
* Cf. {@link JsonWebToken#isActive()}. * Cf. {@link JsonWebToken#isActive()}.
* @author hmlnarik * @author hmlnarik
*/ */
public class TokenNotActiveException extends VerificationException { public class TokenNotActiveException extends TokenVerificationException {


public TokenNotActiveException() { public TokenNotActiveException(JsonWebToken token) {
super(token);
} }


public TokenNotActiveException(String message) { public TokenNotActiveException(JsonWebToken token, String message) {
super(message); super(token, message);
} }


public TokenNotActiveException(String message, Throwable cause) { public TokenNotActiveException(JsonWebToken token, String message, Throwable cause) {
super(message, cause); super(token, message, cause);
} }


public TokenNotActiveException(Throwable cause) { public TokenNotActiveException(JsonWebToken token, Throwable cause) {
super(cause); super(token, cause);
} }


} }
Expand Up @@ -16,27 +16,28 @@
*/ */
package org.keycloak.exceptions; package org.keycloak.exceptions;


import org.keycloak.common.VerificationException; import org.keycloak.representations.JsonWebToken;


/** /**
* Thrown when token signature is invalid. * Thrown when token signature is invalid.
* @author hmlnarik * @author hmlnarik
*/ */
public class TokenSignatureInvalidException extends VerificationException { public class TokenSignatureInvalidException extends TokenVerificationException {


public TokenSignatureInvalidException() { public TokenSignatureInvalidException(JsonWebToken token) {
super(token);
} }


public TokenSignatureInvalidException(String message) { public TokenSignatureInvalidException(JsonWebToken token, String message) {
super(message); super(token, message);
} }


public TokenSignatureInvalidException(String message, Throwable cause) { public TokenSignatureInvalidException(JsonWebToken token, String message, Throwable cause) {
super(message, cause); super(token, message, cause);
} }


public TokenSignatureInvalidException(Throwable cause) { public TokenSignatureInvalidException(JsonWebToken token, Throwable cause) {
super(cause); super(token, cause);
} }


} }

0 comments on commit b55b089

Please sign in to comment.