KEYCLOAK-1856 KEYCLOAK-1860 Fix onoffswitchvalue directive

This commit is contained in:
mposolda 2015-10-02 11:05:46 +02:00
parent ed54ba9064
commit 7816f053a6
18 changed files with 89 additions and 64 deletions

View file

@ -3,6 +3,7 @@ package org.keycloak;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.util.TokenUtil;
import java.io.IOException; import java.io.IOException;
import java.security.PublicKey; import java.security.PublicKey;
@ -13,10 +14,10 @@ import java.security.PublicKey;
*/ */
public class RSATokenVerifier { public class RSATokenVerifier {
public static AccessToken verifyToken(String tokenString, PublicKey realmKey, String realmUrl) throws VerificationException { public static AccessToken verifyToken(String tokenString, PublicKey realmKey, String realmUrl) throws VerificationException {
return verifyToken(tokenString, realmKey, realmUrl, true); return verifyToken(tokenString, realmKey, realmUrl, true, true);
} }
public static AccessToken verifyToken(String tokenString, PublicKey realmKey, String realmUrl, boolean checkActive) throws VerificationException { public static AccessToken verifyToken(String tokenString, PublicKey realmKey, String realmUrl, boolean checkActive, boolean checkTokenType) throws VerificationException {
JWSInput input = null; JWSInput input = null;
try { try {
input = new JWSInput(tokenString); input = new JWSInput(tokenString);
@ -42,6 +43,13 @@ public class RSATokenVerifier {
throw new VerificationException("Token audience doesn't match domain. Token issuer is " + token.getIssuer() + ", but URL from configuration is " + realmUrl); throw new VerificationException("Token audience doesn't match domain. Token issuer is " + token.getIssuer() + ", but URL from configuration is " + realmUrl);
} }
if (checkTokenType) {
String type = token.getType();
if (type == null || !type.equalsIgnoreCase(TokenUtil.TOKEN_TYPE_BEARER)) {
throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + type + "'");
}
}
if (checkActive && !token.isActive()) { if (checkActive && !token.isActive()) {
throw new VerificationException("Token is not active."); throw new VerificationException("Token is not active.");
} }

View file

@ -1,7 +1,6 @@
package org.keycloak.representations; package org.keycloak.representations;
import org.codehaus.jackson.annotate.JsonProperty; import org.keycloak.util.TokenUtil;
import org.keycloak.util.RefreshTokenUtil;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -13,7 +12,7 @@ import java.util.Map;
public class RefreshToken extends AccessToken { public class RefreshToken extends AccessToken {
private RefreshToken() { private RefreshToken() {
type(RefreshTokenUtil.TOKEN_TYPE_REFRESH); type(TokenUtil.TOKEN_TYPE_REFRESH);
} }
/** /**

View file

@ -9,11 +9,15 @@ import org.keycloak.representations.RefreshToken;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class RefreshTokenUtil { public class TokenUtil {
public static final String TOKEN_TYPE_REFRESH = "REFRESH"; public static final String TOKEN_TYPE_BEARER = "Bearer";
public static final String TOKEN_TYPE_OFFLINE = "OFFLINE"; public static final String TOKEN_TYPE_ID = "ID";
public static final String TOKEN_TYPE_REFRESH = "Refresh";
public static final String TOKEN_TYPE_OFFLINE = "Offline";
public static boolean isOfflineTokenRequested(String scopeParam) { public static boolean isOfflineTokenRequested(String scopeParam) {
if (scopeParam == null) { if (scopeParam == null) {

View file

@ -10,6 +10,7 @@ import org.junit.Test;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import org.keycloak.util.TokenUtil;
import javax.security.auth.x500.X500Principal; import javax.security.auth.x500.X500Principal;
import java.io.IOException; import java.io.IOException;
@ -71,7 +72,8 @@ public class RSAVerifierTest {
public void initTest() { public void initTest() {
token = new AccessToken(); token = new AccessToken();
token.subject("CN=Client") token.type(TokenUtil.TOKEN_TYPE_BEARER)
.subject("CN=Client")
.issuer("http://localhost:8080/auth/realm") .issuer("http://localhost:8080/auth/realm")
.addAccess("service").addRole("admin"); .addAccess("service").addRole("admin");
} }

View file

@ -3,7 +3,6 @@ package org.keycloak.example;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import javax.security.cert.X509Certificate; import javax.security.cert.X509Certificate;
@ -22,12 +21,10 @@ import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.ServerRequest; import org.keycloak.adapters.ServerRequest;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.TokenUtil;
import org.keycloak.util.RefreshTokenUtil;
import org.keycloak.util.StreamUtil; import org.keycloak.util.StreamUtil;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import org.keycloak.util.UriUtils; import org.keycloak.util.UriUtils;
@ -64,7 +61,7 @@ public class OfflineAccessPortalServlet extends HttpServlet {
refreshTokenInfo = "No token saved in database. Please login first"; refreshTokenInfo = "No token saved in database. Please login first";
savedTokenAvailable = false; savedTokenAvailable = false;
} else { } else {
RefreshToken refreshTokenDecoded = RefreshTokenUtil.getRefreshToken(refreshToken); RefreshToken refreshTokenDecoded = TokenUtil.getRefreshToken(refreshToken);
String exp = (refreshTokenDecoded.getExpiration() == 0) ? "NEVER" : Time.toDate(refreshTokenDecoded.getExpiration()).toString(); String exp = (refreshTokenDecoded.getExpiration() == 0) ? "NEVER" : Time.toDate(refreshTokenDecoded.getExpiration()).toString();
refreshTokenInfo = String.format("<p>Type: %s</p><p>ID: %s</p><p>Expires: %s</p>", refreshTokenDecoded.getType(), refreshTokenDecoded.getId(), exp); refreshTokenInfo = String.format("<p>Type: %s</p><p>ID: %s</p><p>Expires: %s</p>", refreshTokenDecoded.getType(), refreshTokenDecoded.getId(), exp);
savedTokenAvailable = true; savedTokenAvailable = true;
@ -89,8 +86,8 @@ public class OfflineAccessPortalServlet extends HttpServlet {
RefreshTokenDAO.saveToken(refreshToken); RefreshTokenDAO.saveToken(refreshToken);
RefreshToken refreshTokenDecoded = RefreshTokenUtil.getRefreshToken(refreshToken); RefreshToken refreshTokenDecoded = TokenUtil.getRefreshToken(refreshToken);
Boolean isOfflineToken = refreshTokenDecoded.getType().equals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE); Boolean isOfflineToken = refreshTokenDecoded.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE);
req.setAttribute("isOfflineToken", isOfflineToken); req.setAttribute("isOfflineToken", isOfflineToken);
} }

View file

@ -1535,12 +1535,13 @@ module.directive('onoffswitchstring', function() {
}); });
/** /**
* Directive for presenting an ON-OFF switch for checkbox. * Directive for presenting an ON-OFF switch for checkbox. The directive expects the true-value or false-value to be string like 'true' or 'false', not boolean true/false.
* This directive provides some additional capabilities to the default onoffswitch such as: * This directive provides some additional capabilities to the default onoffswitch such as:
* *
* - Specific scope to specify the value. Instead of just true or false. * - Specific scope to specify the value. Instead of just 'true' or 'false' you can use any other values. For example: true-value="'foo'" false-value="'bar'" .
* But 'true'/'false' are defaults if true-value and false-value are not specified
* *
* Usage: <input ng-model="mmm" name="nnn" id="iii" onoffswitchvalue [on-text="ooo" off-text="fff"] /> * Usage: <input ng-model="mmm" name="nnn" id="iii" onoffswitchvalue [ true-value="'true'" false-value="'false'" on-text="ooo" off-text="fff"] />
*/ */
module.directive('onoffswitchvalue', function() { module.directive('onoffswitchvalue', function() {
return { return {
@ -1549,7 +1550,8 @@ module.directive('onoffswitchvalue', function() {
scope: { scope: {
name: '@', name: '@',
id: '@', id: '@',
value: '=', trueValue: '@',
falseValue: '@',
ngModel: '=', ngModel: '=',
ngDisabled: '=', ngDisabled: '=',
kcOnText: '@onText', kcOnText: '@onText',
@ -1557,7 +1559,7 @@ module.directive('onoffswitchvalue', function() {
}, },
// TODO - The same code acts differently when put into the templateURL. Find why and move the code there. // TODO - The same code acts differently when put into the templateURL. Find why and move the code there.
//templateUrl: "templates/kc-switch.html", //templateUrl: "templates/kc-switch.html",
template: "<span><div class='onoffswitch' tabindex='0'><input type='checkbox' ng-true-value='{{value}}' ng-model='ngModel' ng-disabled='ngDisabled' class='onoffswitch-checkbox' name='{{name}}' id='{{id}}'><label for='{{id}}' class='onoffswitch-label'><span class='onoffswitch-inner'><span class='onoffswitch-active'>{{kcOnText}}</span><span class='onoffswitch-inactive'>{{kcOffText}}</span></span><span class='onoffswitch-switch'></span></label></div></span>", template: "<span><div class='onoffswitch' tabindex='0'><input type='checkbox' ng-true-value='{{trueValue}}' ng-false-value='{{falseValue}}' ng-model='ngModel' ng-disabled='ngDisabled' class='onoffswitch-checkbox' name='{{name}}' id='{{id}}'><label for='{{id}}' class='onoffswitch-label'><span class='onoffswitch-inner'><span class='onoffswitch-active'>{{kcOnText}}</span><span class='onoffswitch-inactive'>{{kcOffText}}</span></span><span class='onoffswitch-switch'></span></label></div></span>",
compile: function(element, attrs) { compile: function(element, attrs) {
/* /*
We don't want to propagate basic attributes to the root element of directive. Id should be passed to the We don't want to propagate basic attributes to the root element of directive. Id should be passed to the
@ -1566,6 +1568,9 @@ module.directive('onoffswitchvalue', function() {
element.removeAttr('name'); element.removeAttr('name');
element.removeAttr('id'); element.removeAttr('id');
if (!attrs.trueValue) { attrs.trueValue = "'true'"; }
if (!attrs.falseValue) { attrs.falseValue = "'false'"; }
if (!attrs.onText) { attrs.onText = "ON"; } if (!attrs.onText) { attrs.onText = "ON"; }
if (!attrs.offText) { attrs.offText = "OFF"; } if (!attrs.offText) { attrs.offText = "OFF"; }

View file

@ -14,7 +14,7 @@
<div class="col-md-6"> <div class="col-md-6">
<input class="form-control" type="text" id="activeSessions" name="activeSessions" data-ng-model="count" ng-disabled="true"> <input class="form-control" type="text" id="activeSessions" name="activeSessions" data-ng-model="count" ng-disabled="true">
</div> </div>
<kc-tooltip>Total number of active offline tokens for this client.</kc-tooltip> <kc-tooltip>Total number of offline tokens for this client.</kc-tooltip>
</div> </div>
</fieldset> </fieldset>
</form> </form>

View file

@ -106,7 +106,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="backchannelSupported">Backchannel Logout</label> <label class="col-sm-2 control-label" for="backchannelSupported">Backchannel Logout</label>
<div class="col-sm-4"> <div class="col-sm-4">
<input ng-model="identityProvider.config.backchannelSupported" id="backchannelSupported" value="'true'" onoffswitchvalue /> <input ng-model="identityProvider.config.backchannelSupported" id="backchannelSupported" onoffswitchvalue />
</div> </div>
<span tooltip-trigger="mouseover mouseout" tooltip-placement="right" tooltip="Does the external IDP support backchannel logout?" class="fa fa-info-circle"></span> <span tooltip-trigger="mouseover mouseout" tooltip-placement="right" tooltip="Does the external IDP support backchannel logout?" class="fa fa-info-circle"></span>
</div> </div>
@ -166,7 +166,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="validateSignature">Validate Signatures</label> <label class="col-md-2 control-label" for="validateSignature">Validate Signatures</label>
<div class="col-md-6"> <div class="col-md-6">
<input ng-model="identityProvider.config.validateSignature" id="validateSignature" value="'true'" onoffswitchvalue /> <input ng-model="identityProvider.config.validateSignature" id="validateSignature" onoffswitchvalue />
</div> </div>
<kc-tooltip>Enable/disable signature validation of external IDP signatures.</kc-tooltip> <kc-tooltip>Enable/disable signature validation of external IDP signatures.</kc-tooltip>
</div> </div>

View file

@ -100,7 +100,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="backchannelSupported">Backchannel Logout</label> <label class="col-sm-2 control-label" for="backchannelSupported">Backchannel Logout</label>
<div class="col-sm-4"> <div class="col-sm-4">
<input ng-model="identityProvider.config.backchannelSupported" id="backchannelSupported" value="'true'" onoffswitchvalue /> <input ng-model="identityProvider.config.backchannelSupported" id="backchannelSupported" onoffswitchvalue />
</div> </div>
<span tooltip-trigger="mouseover mouseout" tooltip-placement="right" tooltip="Does the external IDP support backchannel logout?" class="fa fa-info-circle"></span> <span tooltip-trigger="mouseover mouseout" tooltip-placement="right" tooltip="Does the external IDP support backchannel logout?" class="fa fa-info-circle"></span>
</div> </div>
@ -117,21 +117,21 @@
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="postBindingResponse">HTTP-POST Binding Response</label> <label class="col-md-2 control-label" for="postBindingResponse">HTTP-POST Binding Response</label>
<div class="col-md-6"> <div class="col-md-6">
<input ng-model="identityProvider.config.postBindingResponse" id="postBindingResponse" value="'true'" onoffswitchvalue /> <input ng-model="identityProvider.config.postBindingResponse" id="postBindingResponse" onoffswitchvalue />
</div> </div>
<kc-tooltip>Indicates whether to respond to requests using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used.</kc-tooltip> <kc-tooltip>Indicates whether to respond to requests using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used.</kc-tooltip>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="postBindingAuthnRequest">HTTP-POST Binding for AuthnRequest</label> <label class="col-md-2 control-label" for="postBindingAuthnRequest">HTTP-POST Binding for AuthnRequest</label>
<div class="col-md-6"> <div class="col-md-6">
<input ng-model="identityProvider.config.postBindingAuthnRequest" id="postBindingAuthnRequest" value="'true'" onoffswitchvalue /> <input ng-model="identityProvider.config.postBindingAuthnRequest" id="postBindingAuthnRequest" onoffswitchvalue />
</div> </div>
<kc-tooltip>Indicates whether the AuthnRequest must be sent using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used.</kc-tooltip> <kc-tooltip>Indicates whether the AuthnRequest must be sent using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used.</kc-tooltip>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="wantAuthnRequestsSigned">Want AuthnRequests Signed</label> <label class="col-md-2 control-label" for="wantAuthnRequestsSigned">Want AuthnRequests Signed</label>
<div class="col-md-6"> <div class="col-md-6">
<input ng-model="identityProvider.config.wantAuthnRequestsSigned" id="wantAuthnRequestsSigned" name="wantAuthnRequestsSigned" value="'true'" onoffswitchvalue /> <input ng-model="identityProvider.config.wantAuthnRequestsSigned" id="wantAuthnRequestsSigned" name="wantAuthnRequestsSigned" onoffswitchvalue />
</div> </div>
<kc-tooltip> Indicates whether the identity provider expects signed a AuthnRequest.</kc-tooltip> <kc-tooltip> Indicates whether the identity provider expects signed a AuthnRequest.</kc-tooltip>
</div> </div>
@ -150,14 +150,14 @@
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="forceAuthn">Force Authentication</label> <label class="col-md-2 control-label" for="forceAuthn">Force Authentication</label>
<div class="col-md-6"> <div class="col-md-6">
<input ng-model="identityProvider.config.forceAuthn" id="forceAuthn" name="forceAuthn" value="'true'" onoffswitchvalue /> <input ng-model="identityProvider.config.forceAuthn" id="forceAuthn" name="forceAuthn" onoffswitchvalue />
</div> </div>
<kc-tooltip> Indicates whether the identity provider must authenticate the presenter directly rather than rely on a previous security context.</kc-tooltip> <kc-tooltip> Indicates whether the identity provider must authenticate the presenter directly rather than rely on a previous security context.</kc-tooltip>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="validateSignature">Validate Signature</label> <label class="col-md-2 control-label" for="validateSignature">Validate Signature</label>
<div class="col-md-6"> <div class="col-md-6">
<input ng-model="identityProvider.config.validateSignature" id="validateSignature" value="'true'" onoffswitchvalue /> <input ng-model="identityProvider.config.validateSignature" id="validateSignature" onoffswitchvalue />
</div> </div>
<kc-tooltip>Enable/disable signature validation of SAML responses.</kc-tooltip> <kc-tooltip>Enable/disable signature validation of SAML responses.</kc-tooltip>
</div> </div>

View file

@ -29,7 +29,7 @@
<li ng-class="{active: path[4] == 'offline-access'}" data-ng-show="!client.bearerOnly"> <li ng-class="{active: path[4] == 'offline-access'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/offline-access">Offline Access</a> <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/offline-access">Offline Access</a>
<kc-tooltip>View offline sessions for this client. Allows you to see which users retrieve offline token and when they retrieve it. <kc-tooltip>View offline sessions for this client. Allows you to see which users retrieve offline token and when they retrieve it.
To revoke all tokens for the client, go to Revocation tab and set new not before value. To revoke all tokens for the client, go to Revocation tab and set not before value to now.
</kc-tooltip> </kc-tooltip>
</li> </li>

View file

@ -54,7 +54,7 @@ public class CookieTokenStore {
try { try {
// Skip check if token is active now. It's supposed to be done later by the caller // Skip check if token is active now. It's supposed to be done later by the caller
AccessToken accessToken = RSATokenVerifier.verifyToken(accessTokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl(), false); AccessToken accessToken = RSATokenVerifier.verifyToken(accessTokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl(), false, true);
IDToken idToken; IDToken idToken;
if (idTokenString != null && idTokenString.length() > 0) { if (idTokenString != null && idTokenString.length() > 0) {
JWSInput input = new JWSInput(idTokenString); JWSInput input = new JWSInput(idTokenString);

View file

@ -33,7 +33,7 @@ import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.offline.OfflineTokenUtils; import org.keycloak.services.offline.OfflineTokenUtils;
import org.keycloak.util.RefreshTokenUtil; import org.keycloak.util.TokenUtil;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
@ -96,7 +96,7 @@ public class TokenManager {
UserSessionModel userSession = null; UserSessionModel userSession = null;
ClientSessionModel clientSession = null; ClientSessionModel clientSession = null;
if (RefreshTokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) { if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
clientSession = OfflineTokenUtils.findOfflineClientSession(session, realm, user, oldToken.getClientSession(), oldToken.getSessionState()); clientSession = OfflineTokenUtils.findOfflineClientSession(session, realm, user, oldToken.getClientSession(), oldToken.getSessionState());
if (clientSession != null) { if (clientSession != null) {
@ -168,12 +168,12 @@ public class TokenManager {
.generateIDToken(); .generateIDToken();
// Don't generate refresh token again if refresh was triggered with offline token // Don't generate refresh token again if refresh was triggered with offline token
if (!refreshToken.getType().equals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE)) { if (!refreshToken.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE)) {
responseBuilder.generateRefreshToken(); responseBuilder.generateRefreshToken();
} }
AccessTokenResponse res = responseBuilder.build(); AccessTokenResponse res = responseBuilder.build();
return new RefreshResult(res, RefreshTokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType())); return new RefreshResult(res, TokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType()));
} }
public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken) throws OAuthErrorException { public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken) throws OAuthErrorException {
@ -385,6 +385,7 @@ public class TokenManager {
AccessToken token = new AccessToken(); AccessToken token = new AccessToken();
if (clientSession != null) token.clientSession(clientSession.getId()); if (clientSession != null) token.clientSession(clientSession.getId());
token.id(KeycloakModelUtils.generateId()); token.id(KeycloakModelUtils.generateId());
token.type(TokenUtil.TOKEN_TYPE_BEARER);
token.subject(user.getId()); token.subject(user.getId());
token.audience(client.getClientId()); token.audience(client.getClientId());
token.issuedNow(); token.issuedNow();
@ -487,7 +488,7 @@ public class TokenManager {
} }
String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM); String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
boolean offlineTokenRequested = RefreshTokenUtil.isOfflineTokenRequested(scopeParam); boolean offlineTokenRequested = TokenUtil.isOfflineTokenRequested(scopeParam);
if (offlineTokenRequested) { if (offlineTokenRequested) {
if (!OfflineTokenUtils.isOfflineTokenAllowed(realm, clientSession)) { if (!OfflineTokenUtils.isOfflineTokenAllowed(realm, clientSession)) {
event.error(Errors.NOT_ALLOWED); event.error(Errors.NOT_ALLOWED);
@ -495,7 +496,7 @@ public class TokenManager {
} }
refreshToken = new RefreshToken(accessToken); refreshToken = new RefreshToken(accessToken);
refreshToken.type(RefreshTokenUtil.TOKEN_TYPE_OFFLINE); refreshToken.type(TokenUtil.TOKEN_TYPE_OFFLINE);
OfflineTokenUtils.persistOfflineSession(session, realm, clientSession, userSession); OfflineTokenUtils.persistOfflineSession(session, realm, clientSession, userSession);
} else { } else {
refreshToken = new RefreshToken(accessToken); refreshToken = new RefreshToken(accessToken);
@ -512,6 +513,7 @@ public class TokenManager {
} }
idToken = new IDToken(); idToken = new IDToken();
idToken.id(KeycloakModelUtils.generateId()); idToken.id(KeycloakModelUtils.generateId());
idToken.type(TokenUtil.TOKEN_TYPE_ID);
idToken.subject(accessToken.getSubject()); idToken.subject(accessToken.getSubject());
idToken.audience(client.getClientId()); idToken.audience(client.getClientId());
idToken.issuedNow(); idToken.issuedNow();

View file

@ -119,7 +119,7 @@ public class UserInfoEndpoint {
AccessToken token = null; AccessToken token = null;
try { try {
token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()), true); token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()), true, true);
} catch (VerificationException e) { } catch (VerificationException e) {
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Token invalid: " + e.getMessage(), Status.FORBIDDEN); throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Token invalid: " + e.getMessage(), Status.FORBIDDEN);
} }

View file

@ -42,7 +42,7 @@ public class AppAuthManager extends AuthenticationManager {
public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) { public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
String tokenString = extractAuthorizationHeaderToken(headers); String tokenString = extractAuthorizationHeaderToken(headers);
if (tokenString == null) return null; if (tokenString == null) return null;
AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, true, tokenString, headers); AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, true, true, tokenString, headers);
return authResult; return authResult;
} }

View file

@ -68,6 +68,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import org.keycloak.freemarker.LocaleHelper; import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.util.TokenUtil;
/** /**
* Stateless object that manages authentication * Stateless object that manages authentication
@ -115,7 +116,7 @@ public class AuthenticationManager {
Cookie cookie = headers.getCookies().get(KEYCLOAK_IDENTITY_COOKIE); Cookie cookie = headers.getCookies().get(KEYCLOAK_IDENTITY_COOKIE);
if (cookie == null) return; if (cookie == null) return;
String tokenString = cookie.getValue(); String tokenString = cookie.getValue();
AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()), false); AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()), false, false);
UserSessionModel cookieSession = session.sessions().getUserSession(realm, token.getSessionState()); UserSessionModel cookieSession = session.sessions().getUserSession(realm, token.getSessionState());
if (cookieSession == null || !cookieSession.getId().equals(userSession.getId())) return; if (cookieSession == null || !cookieSession.getId().equals(userSession.getId())) return;
expireIdentityCookie(realm, uriInfo, connection); expireIdentityCookie(realm, uriInfo, connection);
@ -383,7 +384,7 @@ public class AuthenticationManager {
} }
String tokenString = cookie.getValue(); String tokenString = cookie.getValue();
AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, tokenString, session.getContext().getRequestHeaders()); AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, false, tokenString, session.getContext().getRequestHeaders());
if (authResult == null) { if (authResult == null) {
expireIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection()); expireIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
return null; return null;
@ -601,9 +602,10 @@ public class AuthenticationManager {
} }
protected static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, String tokenString, HttpHeaders headers) { protected static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
String tokenString, HttpHeaders headers) {
try { try {
AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()), checkActive); AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()), checkActive, checkTokenType);
if (checkActive) { if (checkActive) {
if (!token.isActive() || token.getIssuedAt() < realm.getNotBefore()) { if (!token.isActive() || token.getIssuedAt() < realm.getNotBefore()) {
logger.debug("identity cookie expired"); logger.debug("identity cookie expired");

View file

@ -9,7 +9,6 @@ import org.junit.rules.TestRule;
import org.junit.runners.model.Statement; import org.junit.runners.model.Statement;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.constants.ServiceAccountConstants; import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.Details; import org.keycloak.events.Details;
@ -24,13 +23,10 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.util.RefreshTokenUtil; import org.keycloak.util.TokenUtil;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -157,7 +153,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
.detail(Details.CODE_ID, codeId) .detail(Details.CODE_ID, codeId)
.detail(Details.TOKEN_ID, isUUID()) .detail(Details.TOKEN_ID, isUUID())
.detail(Details.REFRESH_TOKEN_ID, isUUID()) .detail(Details.REFRESH_TOKEN_ID, isUUID())
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_REFRESH) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH)
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
.session(sessionId); .session(sessionId);
} }
@ -166,7 +162,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
return expect(EventType.REFRESH_TOKEN) return expect(EventType.REFRESH_TOKEN)
.detail(Details.TOKEN_ID, isUUID()) .detail(Details.TOKEN_ID, isUUID())
.detail(Details.REFRESH_TOKEN_ID, refreshTokenId) .detail(Details.REFRESH_TOKEN_ID, refreshTokenId)
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_REFRESH) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH)
.detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID()) .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID())
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
.session(sessionId); .session(sessionId);

View file

@ -2,6 +2,7 @@ package org.keycloak.testsuite.adapter;
import org.junit.Assert; import org.junit.Assert;
import org.keycloak.KeycloakSecurityContext; import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
@ -44,6 +45,15 @@ public class CustomerServlet extends HttpServlet {
Response response = target.request().get(); Response response = target.request().get();
Assert.assertEquals(401, response.getStatus()); Assert.assertEquals(401, response.getStatus());
response.close(); response.close();
// Assert not possible to authenticate with refresh token
RefreshableKeycloakSecurityContext refreshableContext = (RefreshableKeycloakSecurityContext) context;
response = target.request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + refreshableContext.getRefreshToken())
.get();
Assert.assertEquals(401, response.getStatus());
response.close();
String html = target.request() String html = target.request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + context.getTokenString()) .header(HttpHeaders.AUTHORIZATION, "Bearer " + context.getTokenString())
.get(String.class); .get(String.class);

View file

@ -43,7 +43,7 @@ import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule; import org.keycloak.testsuite.rule.WebRule;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.RefreshTokenUtil; import org.keycloak.util.TokenUtil;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import org.keycloak.util.UriUtils; import org.keycloak.util.UriUtils;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
@ -221,10 +221,10 @@ public class OfflineTokenTest {
events.expectCodeToToken(codeId, sessionId) events.expectCodeToToken(codeId, sessionId)
.client("offline-client") .client("offline-client")
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
.assertEvent(); .assertEvent();
Assert.assertEquals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration()); Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId);
@ -278,7 +278,7 @@ public class OfflineTokenTest {
.client("offline-client") .client("offline-client")
.user(userId) .user(userId)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID) .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
.assertEvent(); .assertEvent();
Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID)); Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID));
@ -302,14 +302,14 @@ public class OfflineTokenTest {
.detail(Details.RESPONSE_TYPE, "token") .detail(Details.RESPONSE_TYPE, "token")
.detail(Details.TOKEN_ID, token.getId()) .detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId()) .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
.detail(Details.USERNAME, "test-user@localhost") .detail(Details.USERNAME, "test-user@localhost")
.removeDetail(Details.CODE_ID) .removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI) .removeDetail(Details.REDIRECT_URI)
.removeDetail(Details.CONSENT) .removeDetail(Details.CONSENT)
.assertEvent(); .assertEvent();
Assert.assertEquals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration()); Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
@ -333,11 +333,11 @@ public class OfflineTokenTest {
.session(token.getSessionState()) .session(token.getSessionState())
.detail(Details.TOKEN_ID, token.getId()) .detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId()) .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client") .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
.assertEvent(); .assertEvent();
Assert.assertEquals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration()); Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
@ -356,7 +356,7 @@ public class OfflineTokenTest {
.session(token2.getSessionState()) .session(token2.getSessionState())
.detail(Details.TOKEN_ID, token2.getId()) .detail(Details.TOKEN_ID, token2.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken2.getId()) .detail(Details.REFRESH_TOKEN_ID, offlineToken2.getId())
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client") .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
.assertEvent(); .assertEvent();
@ -371,7 +371,7 @@ public class OfflineTokenTest {
.user(serviceAccountUserId) .user(serviceAccountUserId)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID) .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
.removeDetail(Details.TOKEN_ID) .removeDetail(Details.TOKEN_ID)
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE) .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
.assertEvent(); .assertEvent();
// Refresh with new offline token is ok // Refresh with new offline token is ok
@ -389,7 +389,7 @@ public class OfflineTokenTest {
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri)); Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), RefreshTokenUtil.TOKEN_TYPE_OFFLINE); Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), TokenUtil.TOKEN_TYPE_OFFLINE);
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getExpiration(), 0); Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getExpiration(), 0);
String accessTokenId = OfflineTokenServlet.tokenInfo.accessToken.getId(); String accessTokenId = OfflineTokenServlet.tokenInfo.accessToken.getId();
@ -422,7 +422,7 @@ public class OfflineTokenTest {
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri)); Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), RefreshTokenUtil.TOKEN_TYPE_OFFLINE); Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), TokenUtil.TOKEN_TYPE_OFFLINE);
// Assert refresh works with increased time // Assert refresh works with increased time
Time.setOffset(9999); Time.setOffset(9999);
@ -480,7 +480,7 @@ public class OfflineTokenTest {
oauthGrantPage.accept(); oauthGrantPage.accept();
Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri)); Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), RefreshTokenUtil.TOKEN_TYPE_OFFLINE); Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), TokenUtil.TOKEN_TYPE_OFFLINE);
accountAppPage.open(); accountAppPage.open();
AccountApplicationsPage.AppEntry offlineClient = accountAppPage.getApplications().get("offline-client"); AccountApplicationsPage.AppEntry offlineClient = accountAppPage.getApplications().get("offline-client");