From 1dde26967eadf6f6747dc9f1ad2db18c98bd1f87 Mon Sep 17 00:00:00 2001
From: Konstantinos Georgilakis
Date: Wed, 20 Jul 2022 15:17:25 +0300
Subject: [PATCH] resource request parameter and audience in access token
Closes #13614
---
.../org/keycloak/OAuthErrorException.java | 1 +
.../idm/RealmRepresentation.java | 9 ++++
.../models/cache/infinispan/RealmAdapter.java | 12 +++++
.../infinispan/entities/CachedRealm.java | 6 +++
.../org/keycloak/models/jpa/RealmAdapter.java | 10 +++++
.../models/jpa/entities/RealmAttributes.java | 1 +
.../datastore/LegacyExportImportManager.java | 5 +++
.../map/datastore/MapExportImportManager.java | 5 +++
.../models/map/realm/MapRealmAdapter.java | 11 +++++
.../main/java/org/keycloak/events/Errors.java | 1 +
.../models/utils/ModelToRepresentation.java | 1 +
.../validate/validators/UriValidator.java | 21 +++++++++
.../java/org/keycloak/models/RealmModel.java | 4 ++
.../keycloak/protocol/oidc/TokenManager.java | 26 +++++++++--
.../oidc/endpoints/TokenEndpoint.java | 43 +++++++++++++++---
.../oidc/grants/ciba/CibaGrantType.java | 7 ++-
.../oidc/grants/device/DeviceGrantType.java | 6 ++-
.../keycloak/testsuite/util/OAuthClient.java | 16 +++++++
.../testsuite/oauth/AccessTokenTest.java | 45 +++++++++++++++++++
.../oauth/AuthorizationCodeTest.java | 17 +++++++
.../OAuth2DeviceAuthorizationGrantTest.java | 35 +++++++++++++++
.../testsuite/oauth/OAuthGrantTest.java | 39 ++++++++++++++++
.../testsuite/oauth/RefreshTokenTest.java | 34 ++++++++++++++
.../keycloak/testsuite/util/RealmManager.java | 8 ++++
.../messages/admin-messages_en.properties | 2 +
.../resources/partials/realm-tokens.html | 10 +++++
26 files changed, 362 insertions(+), 13 deletions(-)
diff --git a/core/src/main/java/org/keycloak/OAuthErrorException.java b/core/src/main/java/org/keycloak/OAuthErrorException.java
index df31c6781f2e..a355a73a7e86 100755
--- a/core/src/main/java/org/keycloak/OAuthErrorException.java
+++ b/core/src/main/java/org/keycloak/OAuthErrorException.java
@@ -32,6 +32,7 @@ public class OAuthErrorException extends Exception {
public static final String TEMPORARILY_UNAVAILABLE = "temporarily_unavailable";
public static final String INVALID_REQUEST_URI = "invalid_request_uri";
public static final String INVALID_REQUEST_OBJECT = "invalid_request_object";
+ public static final String INVALID_TARGET ="invalid_target";
// OpenID Connect 1
public static final String INTERACTION_REQUIRED = "interaction_required";
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index 7c820bc7eb05..9aefe09beacb 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -72,6 +72,7 @@ public class RealmRepresentation {
protected Integer oauth2DevicePollingInterval;
protected Boolean enabled;
protected String sslRequired;
+ protected String defaultAudValueForAccessToken;
@Deprecated
protected Boolean passwordCredentialGrantAllowed;
protected Boolean registrationAllowed;
@@ -519,6 +520,14 @@ public void setActionTokenGeneratedByUserLifespan(Integer actionTokenGeneratedBy
this.actionTokenGeneratedByUserLifespan = actionTokenGeneratedByUserLifespan;
}
+ public String getDefaultAudValueForAccessToken() {
+ return defaultAudValueForAccessToken;
+ }
+
+ public void setDefaultAudValueForAccessToken(String defaultAudValueForAccessToken) {
+ this.defaultAudValueForAccessToken = defaultAudValueForAccessToken;
+ }
+
@Deprecated
public List getDefaultRoles() {
return defaultRoles;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 311023c02917..bc84ae9b9147 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -641,6 +641,18 @@ public void setActionTokenGeneratedByUserLifespan(String actionTokenId, Integer
}
}
+ @Override
+ public String getDefaultAudValueForAccessToken() {
+ if (isUpdated()) return updated.getDefaultAudValueForAccessToken();
+ return cached.getDefaultAudValueForAccessToken();
+ }
+
+ @Override
+ public void setDefaultAudValueForAccessToken(String defaultAudValueForAccessToken) {
+ getDelegateForUpdate();
+ updated.setDefaultAudValueForAccessToken(defaultAudValueForAccessToken);
+ }
+
@Override
public Stream getRequiredCredentialsStream() {
if (isUpdated()) return updated.getRequiredCredentialsStream();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index d4916227170e..bc1065a766fa 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -102,6 +102,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected int accessCodeLifespan;
protected int accessCodeLifespanUserAction;
protected int accessCodeLifespanLogin;
+ protected String defaultAudValueForAccessToken;
protected LazyLoader deviceConfig;
protected LazyLoader cibaConfig;
protected LazyLoader parConfig;
@@ -227,6 +228,7 @@ public CachedRealm(Long revision, RealmModel model) {
accessCodeLifespanLogin = model.getAccessCodeLifespanLogin();
actionTokenGeneratedByAdminLifespan = model.getActionTokenGeneratedByAdminLifespan();
actionTokenGeneratedByUserLifespan = model.getActionTokenGeneratedByUserLifespan();
+ defaultAudValueForAccessToken = model.getDefaultAudValueForAccessToken();
notBefore = model.getNotBefore();
passwordPolicy = model.getPasswordPolicy();
otpPolicy = model.getOTPPolicy();
@@ -499,6 +501,10 @@ public int getAccessCodeLifespanLogin() {
return accessCodeLifespanLogin;
}
+ public String getDefaultAudValueForAccessToken() {
+ return defaultAudValueForAccessToken;
+ }
+
public OAuth2DeviceConfig getOAuth2DeviceConfig(Supplier modelSupplier) {
return deviceConfig.get(modelSupplier);
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 41fe39dd30af..4a10fd22e113 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -308,6 +308,16 @@ public void setFailureFactor(int failureFactor) {
setAttribute("failureFactor", failureFactor);
}
+ @Override
+ public String getDefaultAudValueForAccessToken() {
+ return getAttribute(RealmAttributes.DEFAULT_AUD_VALUE_FOR_ACCESS_TOKEN);
+ }
+
+ @Override
+ public void setDefaultAudValueForAccessToken(String defaultAudValueForAccessToken) {
+ setAttribute(RealmAttributes.DEFAULT_AUD_VALUE_FOR_ACCESS_TOKEN, defaultAudValueForAccessToken);
+ }
+
@Override
public boolean isVerifyEmail() {
return realm.isVerifyEmail();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java
index f420e2bb03b6..a838dfb4630b 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java
@@ -39,6 +39,7 @@ public interface RealmAttributes {
String CLIENT_SESSION_MAX_LIFESPAN = "clientSessionMaxLifespan";
String CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT = "clientOfflineSessionIdleTimeout";
String CLIENT_OFFLINE_SESSION_MAX_LIFESPAN = "clientOfflineSessionMaxLifespan";
+ String DEFAULT_AUD_VALUE_FOR_ACCESS_TOKEN = "defaultAudValueForAccessToken";
String WEBAUTHN_POLICY_RP_ENTITY_NAME = "webAuthnPolicyRpEntityName";
String WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = "webAuthnPolicySignatureAlgorithms";
diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java
index 19245e82bd65..0dfcc58eaded 100644
--- a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java
+++ b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java
@@ -218,6 +218,8 @@ public void importRealm(RealmRepresentation rep, RealmModel newRealm, boolean sk
if (rep.getActionTokenGeneratedByUserLifespan() != null)
newRealm.setActionTokenGeneratedByUserLifespan(rep.getActionTokenGeneratedByUserLifespan());
else newRealm.setActionTokenGeneratedByUserLifespan(newRealm.getAccessCodeLifespanUserAction());
+ if (rep.getDefaultAudValueForAccessToken() != null)
+ newRealm.setDefaultAudValueForAccessToken(rep.getDefaultAudValueForAccessToken());
// OAuth 2.0 Device Authorization Grant
OAuth2DeviceConfig deviceConfig = newRealm.getOAuth2DeviceConfig();
@@ -726,6 +728,9 @@ public void updateRealm(RealmRepresentation rep, RealmModel realm) {
realm.setClientOfflineSessionIdleTimeout(rep.getClientOfflineSessionIdleTimeout());
if (rep.getClientOfflineSessionMaxLifespan() != null)
realm.setClientOfflineSessionMaxLifespan(rep.getClientOfflineSessionMaxLifespan());
+ if (rep.getDefaultAudValueForAccessToken() != null)
+ realm.setDefaultAudValueForAccessToken(rep.getDefaultAudValueForAccessToken
+ ());
if (rep.getRequiredCredentials() != null) {
realm.updateRequiredCredentials(rep.getRequiredCredentials());
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java b/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java
index 2c3756e78c07..1cd0243d9a0e 100644
--- a/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java
+++ b/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java
@@ -212,6 +212,9 @@ public void importRealm(RealmRepresentation rep, RealmModel newRealm, boolean sk
newRealm.setActionTokenGeneratedByUserLifespan(rep.getActionTokenGeneratedByUserLifespan());
else newRealm.setActionTokenGeneratedByUserLifespan(newRealm.getAccessCodeLifespanUserAction());
+ if (rep.getDefaultAudValueForAccessToken() != null)
+ newRealm.setDefaultAudValueForAccessToken(rep.getDefaultAudValueForAccessToken());
+
// OAuth 2.0 Device Authorization Grant
OAuth2DeviceConfig deviceConfig = newRealm.getOAuth2DeviceConfig();
@@ -680,6 +683,8 @@ public void updateRealm(RealmRepresentation rep, RealmModel realm) {
realm.setActionTokenGeneratedByAdminLifespan(rep.getActionTokenGeneratedByAdminLifespan());
if (rep.getActionTokenGeneratedByUserLifespan() != null)
realm.setActionTokenGeneratedByUserLifespan(rep.getActionTokenGeneratedByUserLifespan());
+ if (rep.getDefaultAudValueForAccessToken() != null)
+ realm.setDefaultAudValueForAccessToken(rep.getDefaultAudValueForAccessToken());
OAuth2DeviceConfig deviceConfig = realm.getOAuth2DeviceConfig();
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java
index c1ea867a2362..6f08cd4e8443 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java
@@ -83,6 +83,7 @@ public class MapRealmAdapter extends AbstractRealmModel implemen
private static final String MINIMUM_QUICK_LOGIN_WAIT_SECONDS = "minimumQuickLoginWaitSeconds";
private static final String MAX_DELTA_SECONDS = "maxDeltaTimeSeconds";
private static final String FAILURE_FACTOR = "failureFactor";
+ private static final String DEFAULT_AUD_VALUE_FOR_ACCESS_TOKEN = "defaultAudValueForAccessToken";
private PasswordPolicy passwordPolicy;
@@ -455,6 +456,16 @@ public int getAccessCodeLifespanLogin() {
return i == null ? 0 : i;
}
+ @Override
+ public String getDefaultAudValueForAccessToken() {
+ return getAttribute(DEFAULT_AUD_VALUE_FOR_ACCESS_TOKEN);
+ }
+
+ @Override
+ public void setDefaultAudValueForAccessToken(String defaultAudValueForAccessToken) {
+ setAttribute(DEFAULT_AUD_VALUE_FOR_ACCESS_TOKEN, defaultAudValueForAccessToken);
+ }
+
@Override
public void setAccessCodeLifespanLogin(int seconds) {
entity.setAccessCodeLifespanLogin(seconds);
diff --git a/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
index 429be9143cec..1521eea03bbf 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
@@ -49,6 +49,7 @@ public interface Errors {
String INVALID_CODE = "invalid_code";
String INVALID_TOKEN = "invalid_token";
String INVALID_TOKEN_TYPE = "invalid_token_type";
+ String INVALID_TARGET ="invalid_target";
String INVALID_SAML_RESPONSE = "invalid_saml_response";
String INVALID_SAML_AUTHN_REQUEST = "invalid_authn_request";
String INVALID_SAML_LOGOUT_REQUEST = "invalid_logout_request";
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 3c83a781591d..cc6d9cbd3556 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -399,6 +399,7 @@ public static RealmRepresentation toRepresentation(KeycloakSession session, Real
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
rep.setActionTokenGeneratedByAdminLifespan(realm.getActionTokenGeneratedByAdminLifespan());
rep.setActionTokenGeneratedByUserLifespan(realm.getActionTokenGeneratedByUserLifespan());
+ rep.setDefaultAudValueForAccessToken(realm.getDefaultAudValueForAccessToken());
rep.setOAuth2DeviceCodeLifespan(realm.getOAuth2DeviceConfig().getLifespan());
rep.setOAuth2DevicePollingInterval(realm.getOAuth2DeviceConfig().getPoolingInterval());
rep.setSmtpServer(new HashMap<>(realm.getSmtpConfig()));
diff --git a/server-spi-private/src/main/java/org/keycloak/validate/validators/UriValidator.java b/server-spi-private/src/main/java/org/keycloak/validate/validators/UriValidator.java
index f9a6c7874886..7027015ca51c 100644
--- a/server-spi-private/src/main/java/org/keycloak/validate/validators/UriValidator.java
+++ b/server-spi-private/src/main/java/org/keycloak/validate/validators/UriValidator.java
@@ -137,6 +137,27 @@ public boolean validateUri(URI uri, String inputHint, ValidationContext context,
return valid;
}
+ public static void validateUri(String url, Set blockedSchemes, boolean allowFragment, boolean requireValidUrl)
+ throws MalformedURLException, URISyntaxException {
+ URI uri = new URI(url);
+
+ ValidationContext context = new ValidationContext();
+ if (uri.getScheme() != null && blockedSchemes.contains(uri.getScheme())) {
+ throw new MalformedURLException("Not valid URI scheme");
+ }
+
+ if (!allowFragment && uri.getFragment() != null) {
+ throw new MalformedURLException("URI consists fragment");
+ }
+
+ // Don't check if URL is valid if there are other problems with it; otherwise it could lead to duplicate errors.
+ // This cannot be moved higher because it acts on differently based on environment (e.g. sometimes it checks
+ // scheme, sometimes it doesn't).
+ if (requireValidUrl) {
+ URL ignored = uri.toURL(); // throws an exception
+ }
+ }
+
@Override
public String getHelpText() {
return "Uri Validator";
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index 41fb744e24a4..b274d85a3d2e 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -241,6 +241,10 @@ default Boolean getAttribute(String name, Boolean defaultValue) {
void setAccessCodeLifespanUserAction(int seconds);
+ String getDefaultAudValueForAccessToken();
+
+ void setDefaultAudValueForAccessToken(String defaultAudValueForAccessToken);
+
OAuth2DeviceConfig getOAuth2DeviceConfig();
CibaConfig getCibaPolicy();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 3c6a286876dd..c4ca61469cc3 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -126,7 +126,7 @@ public TokenValidation(UserModel user, UserSessionModel userSession, ClientSessi
}
public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm,
- RefreshToken oldToken, HttpHeaders headers) throws OAuthErrorException {
+ RefreshToken oldToken, HttpHeaders headers, List resourceList) throws OAuthErrorException {
UserSessionModel userSession = null;
boolean offline = TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType());
@@ -219,7 +219,7 @@ public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, C
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, oldToken.getNonce());
// recreate token.
- AccessToken newToken = createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
+ AccessToken newToken = createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx, resourceList);
return new TokenValidation(user, userSession, clientSessionCtx, newToken);
}
@@ -359,14 +359,14 @@ public static UserModel lookupUserFromStatelessToken(KeycloakSession session, Re
public AccessTokenResponseBuilder refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient,
- String encodedRefreshToken, EventBuilder event, HttpHeaders headers, HttpRequest request) throws OAuthErrorException {
+ String encodedRefreshToken, EventBuilder event, HttpHeaders headers, HttpRequest request, List resourceList) throws OAuthErrorException {
RefreshToken refreshToken = verifyRefreshToken(session, realm, authorizedClient, request, encodedRefreshToken, true);
event.user(refreshToken.getSubject()).session(refreshToken.getSessionState())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, refreshToken.getType());
- TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken, headers);
+ TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken, headers, resourceList);
AuthenticatedClientSessionModel clientSession = validation.clientSessionCtx.getClientSession();
// validate authorizedClient is same as validated client
@@ -532,6 +532,18 @@ public AccessToken createClientAccessToken(KeycloakSession session, RealmModel r
return token;
}
+ public AccessToken createClientAccessToken(KeycloakSession session, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession,
+ ClientSessionContext clientSessionCtx, List resourceList) {
+ AccessToken token = initToken(realm, client, user, userSession, clientSessionCtx, session.getContext().getUri());
+ if (resourceList !=null) {
+ token.audience(resourceList.toArray(new String[resourceList.size()]));
+ } else if (realm.getDefaultAudValueForAccessToken() != null && !realm.getDefaultAudValueForAccessToken().isEmpty()) {
+ token.audience(realm.getDefaultAudValueForAccessToken());
+ }
+ token = transformAccessToken(session, token, userSession, clientSessionCtx);
+ return token;
+ }
+
public static ClientSessionContext attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession, AuthenticationSessionModel authSession) {
ClientModel client = authSession.getClient();
@@ -1036,6 +1048,12 @@ public AccessTokenResponseBuilder generateAccessToken() {
return this;
}
+ public AccessTokenResponseBuilder generateAccessToken(List resourceList) {
+ UserModel user = userSession.getUser();
+ accessToken = createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx, resourceList);
+ return this;
+ }
+
public AccessTokenResponseBuilder generateRefreshToken() {
if (accessToken == null) {
throw new IllegalStateException("accessToken not set");
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 6e612b5703ee..b09a54526ecd 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -92,6 +92,7 @@
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
import org.keycloak.utils.ProfileHelper;
+import org.keycloak.validate.validators.UriValidator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -110,10 +111,14 @@
import javax.xml.namespace.QName;
import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForModification;
@@ -297,7 +302,7 @@ private void checkGrantType() {
private void checkParameterDuplicated() {
for (String key : formParams.keySet()) {
- if (formParams.get(key).size() != 1) {
+ if (formParams.get(key).size() != 1 && !OAuth2Constants.RESOURCE.equals(key)) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "duplicated parameter",
Response.Status.BAD_REQUEST);
}
@@ -311,6 +316,9 @@ public Response codeToToken() {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
}
+ List resourceList = formParams.get(OAuth2Constants.RESOURCE);
+ checkResourceList(resourceList, event, cors);
+
OAuth2CodeParser.ParseResult parseResult = OAuth2CodeParser.parseCode(session, code, realm, event);
if (parseResult.isIllegalCode()) {
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
@@ -431,12 +439,12 @@ public Response codeToToken() {
// Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, codeData.getNonce());
- return createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true);
+ return createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true, resourceList);
}
public Response createTokenResponse(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx,
- String scopeParam, boolean code) {
- AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
+ String scopeParam, boolean code, List resourceList) {
+ AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx, resourceList);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager
.responseBuilder(realm, client, event, session, userSession, clientSessionCtx).accessToken(token);
@@ -501,10 +509,13 @@ public Response refreshTokenGrant() {
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}
+ List resourceList = formParams.get(OAuth2Constants.RESOURCE);
+ checkResourceList(resourceList, event, cors);
+
AccessTokenResponse res;
try {
// KEYCLOAK-6771 Certificate Bound Token
- TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.refreshAccessToken(session, session.getContext().getUri(), clientConnection, realm, client, refreshToken, event, headers, request);
+ TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.refreshAccessToken(session, session.getContext().getUri(), clientConnection, realm, client, refreshToken, event, headers, request, resourceList);
res = responseBuilder.build();
if (!responseBuilder.isOfflineToken()) {
@@ -581,6 +592,9 @@ public Response resourceOwnerPasswordCredentialsGrant() {
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}
+ List resourceList = formParams.get(OAuth2Constants.RESOURCE);
+ checkResourceList(resourceList, event, cors);
+
String scope = getRequestedScopes();
RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
@@ -626,7 +640,7 @@ public Response resourceOwnerPasswordCredentialsGrant() {
updateUserSessionFromClientAuth(userSession);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager
- .responseBuilder(realm, client, event, session, userSession, clientSessionCtx).generateAccessToken();
+ .responseBuilder(realm, client, event, session, userSession, clientSessionCtx).generateAccessToken(resourceList);
if (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken()) {
responseBuilder.generateRefreshToken();
}
@@ -660,6 +674,9 @@ public Response clientCredentialsGrant() {
throw new CorsErrorResponseException(cors, OAuthErrorException.UNAUTHORIZED_CLIENT, "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
}
+ List resourceList = formParams.get(OAuth2Constants.RESOURCE);
+ checkResourceList(resourceList, event, cors);
+
UserModel clientUser = session.users().getServiceAccount(client);
if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
@@ -719,7 +736,7 @@ public Response clientCredentialsGrant() {
updateUserSessionFromClientAuth(userSession);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSessionCtx)
- .generateAccessToken();
+ .generateAccessToken(resourceList);
// Make refresh token generation optional, see KEYCLOAK-9551
if (useRefreshToken) {
@@ -952,6 +969,18 @@ public Response cibaGrant() {
return grantType.cibaGrant();
}
+ public static void checkResourceList(List resourceList, EventBuilder event, Cors cors ) {
+ try {
+ if (resourceList != null)
+ for (String resource : resourceList) {
+ UriValidator.validateUri(resource, Stream.of("data","javascript").collect(Collectors.toSet()), true, false);
+ }
+ } catch (MalformedURLException | URISyntaxException e) {
+ event.error(Errors.INVALID_TARGET);
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TARGET, "The requested resource is invalid or malformed.", Response.Status.BAD_REQUEST);
+ }
+ }
+
public static class TokenExchangeSamlProtocol extends SamlProtocol {
final SamlClient samlClient;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java
index 6a90a8c26d71..f86d36c522ac 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java
@@ -23,6 +23,7 @@
import javax.ws.rs.core.UriBuilder;
import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.Profile;
@@ -60,6 +61,7 @@
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.utils.ProfileHelper;
+import java.util.List;
import java.util.Map;
/**
@@ -140,6 +142,9 @@ public Response cibaGrant() {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + AUTH_REQ_ID, Response.Status.BAD_REQUEST);
}
+ List resourceList = formParams.get(OAuth2Constants.RESOURCE);
+ TokenEndpoint.checkResourceList(resourceList, event, cors);
+
logger.tracev("CIBA Grant :: authReqId = {0}", jwe);
CIBAAuthenticationRequest request;
@@ -216,7 +221,7 @@ public Response cibaGrant() {
int authTime = Time.currentTime();
userSession.setNote(AuthenticationManager.AUTH_TIME, String.valueOf(authTime));
- return tokenEndpoint.createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true);
+ return tokenEndpoint.createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true, resourceList);
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java
index 313dcaaf34ef..bb3ae06a10a9 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java
@@ -61,6 +61,7 @@
import javax.ws.rs.core.UriInfo;
import java.net.URI;
+import java.util.List;
import java.util.Map;
/**
@@ -256,6 +257,9 @@ public Response oauth2DeviceFlow() {
"The authorization request is still pending", Response.Status.BAD_REQUEST);
}
+ List resourceList = formParams.get(OAuth2Constants.RESOURCE);
+ TokenEndpoint.checkResourceList(resourceList, event, cors);
+
// https://tools.ietf.org/html/rfc7636#section-4.6
String codeVerifier = formParams.getFirst(OAuth2Constants.CODE_VERIFIER);
String codeChallenge = deviceCodeModel.getCodeChallenge();
@@ -341,6 +345,6 @@ public Response oauth2DeviceFlow() {
// Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, deviceCodeModel.getNonce());
- return tokenEndpoint.createTokenResponse(user, userSession, clientSessionCtx, scopeParam, false);
+ return tokenEndpoint.createTokenResponse(user, userSession, clientSessionCtx, scopeParam, false, resourceList);
}
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 993fc5b2b386..38890198af6f 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -183,6 +183,8 @@ public static void resetAppRootRealm() {
private String requestUri;
+ private String resource;
+
private String claims;
private Map requestHeaders;
@@ -506,6 +508,9 @@ public AccessTokenResponse doAccessTokenRequest(String code, String password, Cl
if (codeVerifier != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
}
+ if (resource != null) {
+ parameters.add(new BasicNameValuePair(OAuth2Constants.RESOURCE, resource));
+ }
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, Charsets.UTF_8);
post.setEntity(formEntity);
@@ -1000,6 +1005,9 @@ public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String pas
if (clientSessionHost != null) {
parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost));
}
+ if (resource != null) {
+ parameters.add(new BasicNameValuePair(OAuth2Constants.RESOURCE, resource));
+ }
UrlEncodedFormEntity formEntity;
try {
@@ -1044,6 +1052,9 @@ public DeviceAuthorizationResponse doDeviceAuthorizationRequest(String clientId,
if (codeChallengeMethod != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_CHALLENGE_METHOD, codeChallengeMethod));
}
+ if (resource != null) {
+ parameters.add(new BasicNameValuePair(OAuth2Constants.RESOURCE, resource));
+ }
UrlEncodedFormEntity formEntity;
try {
@@ -1619,6 +1630,11 @@ public OAuthClient redirectUri(String redirectUri) {
return this;
}
+ public OAuthClient resource(String resource) {
+ this.resource = resource;
+ return this;
+ }
+
public OAuthClient postLogoutRedirectUri(String postLogoutRedirectUri) {
this.postLogoutRedirectUri = postLogoutRedirectUri;
return this;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 2343081f0c5e..941444605962 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -246,6 +246,9 @@ public void accessTokenRequest() throws Exception {
assertEquals(oauth.parseRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
assertEquals(sessionId, token.getSessionState());
+ //by default audience without resource parameter, defaultAudValueForAccessToken be set and scope with audience audience must be null
+ Assert.assertNull(token.getAudience());
+
}
@Test
@@ -1423,4 +1426,46 @@ public void tokenRequestParamsMoreThanOnce() throws Exception {
}
}
+ @Test
+ public void tokenRequestWithResource() throws IOException {
+ oauth.resource("https://www.keycloak.org/documentation");
+
+ try {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+ Assert.assertEquals(200, response.getStatusCode());
+ Assert.assertNotNull(response.getAccessToken());
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ Assert.assertTrue(accessToken.hasAudience("https://www.keycloak.org/documentation"));
+
+ } finally {
+ oauth.resource(null);
+ }
+ }
+
+ @Test
+ public void tokenRequestWithDefaultAudienceValue() throws IOException {
+ RealmManager.realm(adminClient.realm("test")).defaultAudValueForAccessToken("https://www.keycloak.org/");
+
+ try {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+ Assert.assertEquals(200, response.getStatusCode());
+ Assert.assertNotNull(response.getAccessToken());
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ Assert.assertTrue(accessToken.hasAudience("https://www.keycloak.org/"));
+
+ } finally {
+ RealmManager.realm(adminClient.realm("test")).defaultAudValueForAccessToken(null);
+ }
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
index 03c44553b961..5837ead9327f 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
@@ -27,6 +27,7 @@
import org.keycloak.events.Errors;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
+import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
@@ -38,6 +39,7 @@
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
+import java.net.MalformedURLException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
@@ -131,6 +133,21 @@ public void testInvalidRedirectUri() {
assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
}
+// @Test
+// public void testInvalidResourceParameter() throws MalformedURLException {
+// oauth.resource("data://www.keycloak.org/guides");
+//
+// UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
+// driver.navigate().to(b.build().toURL());
+//
+// String error = driver.findElement(By.id("error")).getText();
+// String errorDescription = driver.findElement(By.id("error_description")).getText();
+// assertEquals(OAuthErrorException.INVALID_TARGET, error);
+// assertEquals("The requested resource is invalid or malformed.", errorDescription);
+//
+// oauth.resource(null);
+// }
+
@Test
public void authorizationRequestNoState() throws IOException {
oauth.stateParamHardcoded(null);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java
index 2fb110d443c4..dae9c3f42394 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java
@@ -217,6 +217,41 @@ public void testPublicClient() throws Exception {
assertNotNull(token);
}
+ @Test
+ public void testPublicClientWithResource() throws Exception {
+ // Device Authorization Request from device
+ oauth.realm(REALM_NAME);
+ oauth.clientId(DEVICE_APP_PUBLIC);
+ OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP_PUBLIC, null);
+
+ Assert.assertEquals(200, response.getStatusCode());
+ assertNotNull(response.getDeviceCode());
+ assertNotNull(response.getUserCode());
+ assertNotNull(response.getVerificationUri());
+ assertNotNull(response.getVerificationUriComplete());
+ Assert.assertEquals(60, response.getExpiresIn());
+ Assert.assertEquals(5, response.getInterval());
+
+ openVerificationPage(response.getVerificationUriComplete());
+
+ // Do Login
+ oauth.fillLoginForm("device-login", "password");
+
+ // Consent
+ grantPage.accept();
+
+ // Token request from device
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP_PUBLIC, null, response.getDeviceCode());
+
+ Assert.assertEquals(200, tokenResponse.getStatusCode());
+
+ String tokenString = tokenResponse.getAccessToken();
+ assertNotNull(tokenString);
+ AccessToken token = oauth.verifyToken(tokenString);
+
+ assertNotNull(token);
+ }
+
@Test
public void testPublicClientOptionalScope() throws Exception {
// Device Authorization Request from device - check giving optional scope phone
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
index 2e13634c4767..4b8ca92a27e8 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
@@ -22,6 +22,7 @@
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientScopeResource;
import org.keycloak.admin.client.resource.RealmResource;
@@ -59,6 +60,7 @@
import javax.ws.rs.core.Response;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
@@ -146,6 +148,43 @@ public void oauthGrantAcceptTest() {
assertEquals(0, driver.findElements(By.id("revoke-third-party")).size());
}
+ @Test
+ public void oauthGrantTestWithResourceParameter() {
+ try {
+ oauth.resource("https://www.keycloak.org/documentation");
+ oauth.clientId(THIRD_PARTY_APP);
+ oauth.doLoginGrant("test-user@localhost", "password");
+
+ grantPage.assertCurrent();
+ grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
+
+ grantPage.accept();
+
+ Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.CODE));
+
+ EventRepresentation loginEvent = events.expectLogin()
+ .client(THIRD_PARTY_APP)
+ .detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
+ .assertEvent();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+ String sessionId = loginEvent.getSessionId();
+
+ OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
+
+ Assert.assertNotNull(accessTokenResponse.getAccessToken());
+ AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
+ Assert.assertTrue(accessToken.hasAudience("https://www.keycloak.org/documentation"));
+
+ accountAppsPage.open();
+
+ assertEquals(1, driver.findElements(By.id("revoke-third-party")).size());
+
+ accountAppsPage.revokeGrant(THIRD_PARTY_APP);
+ } finally {
+ oauth.resource(null);
+ }
+ }
+
@Test
public void oauthGrantCancelTest() {
oauth.clientId(THIRD_PARTY_APP);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index 82f6942b9962..8790a6608ae3 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -198,6 +198,40 @@ public void refreshTokenStructure() {
assertTrue("ResourceAccess should be null for RefreshTokens", refreshToken.getResourceAccess().isEmpty());
}
+ @Test
+ public void refreshTokenFlowWithResourceParameter() {
+
+ try {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+ AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+ Assert.assertFalse(token.hasAudience("https://www.keycloak.org/documentation"));
+
+ String refreshTokenString = tokenResponse.getRefreshToken();
+ events.expectCodeToToken(codeId, sessionId).assertEvent();
+
+ assertNotNull(refreshTokenString);
+
+ oauth.resource("https://www.keycloak.org/documentation");
+ OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
+ assertEquals(200, response.getStatusCode());
+ assertNotNull(response.getRefreshToken());
+ AccessToken refreshedAccessToken = oauth.verifyToken(response.getAccessToken());
+ Assert.assertTrue(refreshedAccessToken.hasAudience("https://www.keycloak.org/documentation"));
+
+ } finally {
+ oauth.resource(null);
+ }
+ }
+
@Test
public void refreshTokenRequest() throws Exception {
oauth.nonce("123456");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmManager.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmManager.java
index 9d70b075a646..433f0f0139b3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmManager.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmManager.java
@@ -172,4 +172,12 @@ public RealmManager ssoSessionIdleTimeout(int ssoSessionIdleTimeout) {
realm.update(rep);
return this;
}
+
+ public RealmManager defaultAudValueForAccessToken(String defaultAudValueForAccessToken) {
+ RealmRepresentation realmRepresentation = realm.toRepresentation();
+ realmRepresentation.setDefaultAudValueForAccessToken(defaultAudValueForAccessToken);
+ realm.update(realmRepresentation);
+ return this;
+ }
+
}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index ba9ca09757e2..0d06098e8979 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -178,6 +178,8 @@ login-timeout=Login timeout
login-timeout.tooltip=Max time a user has to complete a login. This is recommended to be relatively long, such as 30 minutes or more.
login-action-timeout=Login action timeout
login-action-timeout.tooltip=Max time a user has to complete login related actions like update password or configure totp. This is recommended to be relatively long, such as 5 minutes or more.
+default-aud-value-access-token=Default aud value for access token
+default-aud-value-access-token.tooltip=Default optional aud value for access token. If resource parameter is not present, aud claim is set equal to this value. Extra aud claim values can be added with Audience and AudienceResolver protocol mappers.
oauth2-device-code-lifespan=OAuth 2.0 Device Code Lifespan
oauth2-device-code-lifespan.tooltip=Max time before the device code and user code are expired. This value needs to be a long enough lifetime to be usable (allowing the user to retrieve their secondary device, navigate to the verification URI, login, etc.), but should be sufficiently short to limit the usability of a code obtained for phishing.
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
index 65232d2de568..dceed791a203 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
@@ -286,6 +286,16 @@
+
+