diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/TokenManager.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/TokenManager.java index b1a36a0ad..b14a50602 100644 --- a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/TokenManager.java +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/TokenManager.java @@ -26,30 +26,45 @@ package org.eclipse.digitaltwin.basyx.client.internal.authorization; import java.io.IOException; +import java.text.ParseException; import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.AccessTokenProvider; import org.eclipse.digitaltwin.basyx.core.exceptions.AccessTokenRetrievalException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.nimbusds.oauth2.sdk.AccessTokenResponse; import com.nimbusds.oauth2.sdk.token.AccessToken; import com.nimbusds.oauth2.sdk.token.RefreshToken; +import net.minidev.json.JSONObject; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; + +import java.util.Date; + /** * Requests and manages the Access Tokens and Refresh Tokens. * - * @author danish + * @author danish */ +import java.time.Instant; + public class TokenManager { - - private String tokenEndpoint; - private AccessTokenProvider accessTokenProvider; + + private static final Logger LOGGER = LoggerFactory.getLogger(TokenManager.class); + + private static final String EXPIRES_IN = "expires_in"; + private static final String REFRESH_EXPIRES_IN = "refresh_expires_in"; + private final String tokenEndpoint; + private final AccessTokenProvider accessTokenProvider; private String accessToken; - private String refreshToken; - private long accessTokenExpiryTime; - private long refreshTokenExpiryTime; - + private String refreshToken; + private Instant accessTokenExpiryTime; + private Instant refreshTokenExpiryTime; + public TokenManager(String tokenEndpoint, AccessTokenProvider accessTokenProvider) { - super(); this.tokenEndpoint = tokenEndpoint; this.accessTokenProvider = accessTokenProvider; } @@ -61,46 +76,139 @@ public String getTokenEndpoint() { public AccessTokenProvider getAccessTokenProvider() { return this.accessTokenProvider; } - + + /** + * Provides the access token, refreshing it if necessary. + * + * @return the current valid access token + * @throws IOException + * if an error occurs while retrieving the token + */ + public String getAccessToken() throws IOException { + Instant currentTime = Instant.now(); + + if (accessToken != null && currentTime.isBefore(accessTokenExpiryTime)) + return accessToken; + + synchronized (this) { + if (accessToken != null && currentTime.isBefore(accessTokenExpiryTime)) + return accessToken; + + if (refreshToken != null && currentTime.isBefore(refreshTokenExpiryTime)) + return refreshAccessToken(currentTime); + + return obtainNewAccessToken(currentTime); + } + } + /** - * Provides access token + * Updates the tokens and their expiry times. * - * @return accessToken + * @param accessTokenResponse + * the response containing the new tokens + * @param currentTime + * the current timestamp for consistency + * @return the new access token * @throws IOException + * if an error occurs while processing the response + */ + private String updateTokens(AccessTokenResponse accessTokenResponse, Instant currentTime) throws IOException { + AccessToken accessTokenObj = accessTokenResponse.getTokens().getAccessToken(); + accessToken = accessTokenObj.getValue(); + accessTokenExpiryTime = calculateExpiryTime(accessTokenObj, currentTime); + + RefreshToken refreshTokenObj = accessTokenResponse.getTokens().getRefreshToken(); + + if (refreshTokenObj != null) { + refreshToken = refreshTokenObj.getValue(); + refreshTokenExpiryTime = calculateRefreshExpiryTime(refreshTokenObj, accessTokenResponse, currentTime); + } + + return accessToken; + } + + /** + * Calculates the expiry time for a JWT token. First checks the 'exp' field in + * the JWT, falling back to 'expires_in'. + * + * @param tokenObj + * the AccessToken or RefreshToken object + * @param currentTime + * the current timestamp + * @return the calculated expiry time as Instant + */ + private Instant calculateExpiryTime(AccessToken tokenObj, Instant currentTime) { + String tokenValue = tokenObj.getValue(); + Date expirationDate = extractExpirationTimeAsDateFromToken(tokenValue); + + if (expirationDate != null) + return expirationDate.toInstant(); + + LOGGER.info("Unable to find 'exp' claim inside Access Token! Falling back to the alternative, the '{}' field.", EXPIRES_IN); + + return currentTime.plusSeconds(tokenObj.getLifetime()); + } + + /** + * Calculates the expiry time for a refresh token. First checks the 'exp' field + * in the JWT refresh token, falling back to 'refresh_expires_in'. + * + * @param refreshTokenObj + * the RefreshToken object + * @param accessTokenResponse + * the response containing the refresh token + * @param currentTime + * the current timestamp + * @return the calculated expiry time as Instant */ - public synchronized String getAccessToken() throws IOException { + private Instant calculateRefreshExpiryTime(RefreshToken refreshTokenObj, AccessTokenResponse accessTokenResponse, Instant currentTime) { + String tokenValue = refreshTokenObj.getValue(); + Date expirationDate = extractExpirationTimeAsDateFromToken(tokenValue); + + if (expirationDate != null) + return expirationDate.toInstant(); + + LOGGER.info("Unable to find 'exp' claim inside Refresh Token! Falling back to the alternative, the '{}' field", REFRESH_EXPIRES_IN); - if (accessToken != null && System.currentTimeMillis() < accessTokenExpiryTime) - return accessToken; + JSONObject jsonObject = accessTokenResponse.toJSONObject(); + Number refreshExpiresInSeconds = jsonObject.getAsNumber(REFRESH_EXPIRES_IN); - if (refreshToken != null && System.currentTimeMillis() < refreshTokenExpiryTime) { - try { - return requestAccessToken(accessTokenProvider.getAccessTokenResponse(tokenEndpoint, refreshToken)); - } catch (IOException e) { - throw new AccessTokenRetrievalException("Error occurred while retrieving access token" + e.getMessage()); + if (refreshExpiresInSeconds == null) + return Instant.EPOCH; + + return currentTime.plusSeconds(refreshExpiresInSeconds.longValue()); + } + + private Date extractExpirationTimeAsDateFromToken(String tokenValue) { + try { + JWT jwt = JWTParser.parse(tokenValue); + + if (jwt instanceof SignedJWT) { + SignedJWT signedJwt = (SignedJWT) jwt; + return signedJwt.getJWTClaimsSet().getExpirationTime(); } - } + } catch (ParseException e) { + LOGGER.error("Failed to parse the token. Invalid JWT format: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error occurred while extracting expiration time from the Token: " + e.getMessage()); + } + + return null; + } + + private String obtainNewAccessToken(Instant currentTime) { + try { + return updateTokens(accessTokenProvider.getAccessTokenResponse(tokenEndpoint), currentTime); + } catch (IOException e) { + throw new AccessTokenRetrievalException("Error occurred while retrieving access token: " + e.getMessage()); + } + } - try { - return requestAccessToken(accessTokenProvider.getAccessTokenResponse(tokenEndpoint)); + private String refreshAccessToken(Instant currentTime) { + try { + return updateTokens(accessTokenProvider.getAccessTokenResponse(tokenEndpoint, refreshToken), currentTime); } catch (IOException e) { - throw new AccessTokenRetrievalException("Error occurred while retrieving access token" + e.getMessage()); + throw new AccessTokenRetrievalException("Error occurred while retrieving access token: " + e.getMessage()); } - } - - private String requestAccessToken(AccessTokenResponse accessTokenResponse) throws IOException { - AccessToken accessTokenObj = accessTokenResponse.getTokens().getAccessToken(); - accessToken = accessTokenObj.getValue(); - accessTokenExpiryTime = accessTokenObj.getLifetime(); - - RefreshToken refreshTokenObj = accessTokenResponse.getTokens().getRefreshToken(); - - if (refreshTokenObj != null) { - refreshToken = refreshTokenObj.getValue(); - refreshTokenExpiryTime = System.currentTimeMillis() + (30L * 24L * 60L * 60L * 1000L); - } - - return accessToken; - } - + } } diff --git a/basyx.common/basyx.client/src/test/java/org/eclipse/digitaltwin/basyx/client/internal/TokenManagerTest.java b/basyx.common/basyx.client/src/test/java/org/eclipse/digitaltwin/basyx/client/internal/TokenManagerTest.java index e8ee9e9b0..ce674202d 100644 --- a/basyx.common/basyx.client/src/test/java/org/eclipse/digitaltwin/basyx/client/internal/TokenManagerTest.java +++ b/basyx.common/basyx.client/src/test/java/org/eclipse/digitaltwin/basyx/client/internal/TokenManagerTest.java @@ -25,38 +25,38 @@ package org.eclipse.digitaltwin.basyx.client.internal; +import static org.junit.Assert.*; import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; -import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; -import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.ClientCredentialAccessTokenProvider; import org.junit.Before; import org.junit.Test; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.PasswordCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.PasswordCredentialAccessTokenProvider; import java.io.IOException; -import static org.junit.Assert.*; - /** * Tests the behaviour of {@link TokenManager} * * @author danish */ public class TokenManagerTest { - + + private static final String TOKEN_ENDPOINT = "http://localhost:9096/realms/BaSyx/protocol/openid-connect/token"; private TokenManager tokenManager; @Before public void setUp() { - tokenManager = new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom"))); + tokenManager = new TokenManager(TOKEN_ENDPOINT, new PasswordCredentialAccessTokenProvider(new PasswordCredential("basyx.reader", "basyxreader"), new ClientCredential("max-sso", "8ccc227xJflxGgtFkwwssHRZUh99nAAc"))); } @Test - public void testGetAccessToken_RetrievesNewTokenAfterExpiry() throws IOException, InterruptedException { - + public void retrieveNewAccessTokenWhenExpired() throws IOException, InterruptedException { String initialAccessToken = tokenManager.getAccessToken(); assertNotNull(initialAccessToken); - long tokenLifetime = 5000; - Thread.sleep(tokenLifetime + 1000); + long tokenLifetime = 1000; + Thread.sleep(tokenLifetime + 5); String newAccessToken = tokenManager.getAccessToken(); assertNotNull(newAccessToken); @@ -64,4 +64,67 @@ public void testGetAccessToken_RetrievesNewTokenAfterExpiry() throws IOException assertNotEquals(initialAccessToken, newAccessToken); } + @Test + public void provideSameAccessTokenWhenNotExpired() throws IOException, InterruptedException { + String initialAccessToken = tokenManager.getAccessToken(); + assertNotNull(initialAccessToken); + + long tokenLifetime = 350; + Thread.sleep(tokenLifetime); + + String newAccessToken = tokenManager.getAccessToken(); + assertNotNull(newAccessToken); + + assertEquals(initialAccessToken, newAccessToken); + } + + @Test + public void retrieveNewAccessTokenUsingRefreshTokenWhenAccessTokenIsExpired() throws IOException, InterruptedException { + String initialAccessToken = tokenManager.getAccessToken(); + assertNotNull(initialAccessToken); + + long tokenLifetime = 1000; + Thread.sleep(tokenLifetime + 15); + + String newAccessToken = tokenManager.getAccessToken(); + assertNotNull(newAccessToken); + + assertNotEquals(initialAccessToken, newAccessToken); + } + + @Test + public void retrieveNewAccessTokenUsingRefreshTokenWhenExpired() throws IOException, InterruptedException { + String initialAccessToken = tokenManager.getAccessToken(); + assertNotNull(initialAccessToken); + + long tokenLifetime = 3000; + Thread.sleep(tokenLifetime + 5); + + String newAccessToken = tokenManager.getAccessToken(); + assertNotNull(newAccessToken); + + assertNotEquals(initialAccessToken, newAccessToken); + } + + @Test + // This test is for verifying the behavior described in the issue: https://github.com/eclipse-basyx/basyx-java-server-sdk/issues/530 + public void retrieveNewAccessTokenWhenSSOMaxReached() throws IOException, InterruptedException { + String initialAccessToken = tokenManager.getAccessToken(); + assertNotNull(initialAccessToken); + + long timeLimit = 4000; + String intermediateAccessToken = null; + + while (timeLimit > 0) { + intermediateAccessToken = tokenManager.getAccessToken(); + + Thread.sleep(2000); + + timeLimit -= 2000; + } + + assertNotNull(intermediateAccessToken); + assertNotEquals(initialAccessToken, intermediateAccessToken); + } + } diff --git a/ci/keycloak/realm/BaSyx-realm.json b/ci/keycloak/realm/BaSyx-realm.json index 2b28f4c58..725b7d0af 100644 --- a/ci/keycloak/realm/BaSyx-realm.json +++ b/ci/keycloak/realm/BaSyx-realm.json @@ -8,7 +8,7 @@ "accessTokenLifespan" : 300, "accessTokenLifespanForImplicitFlow" : 900, "ssoSessionIdleTimeout" : 1800, - "ssoSessionMaxLifespan" : 36000, + "ssoSessionMaxLifespan" : 900, "ssoSessionIdleTimeoutRememberMe" : 0, "ssoSessionMaxLifespanRememberMe" : 0, "offlineSessionIdleTimeout" : 2592000, @@ -76,6 +76,14 @@ "clientRole" : false, "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", "attributes" : { } + }, { + "id" : "54605a65-8657-44cd-8617-fe2aba9105e0", + "name" : "abc", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } }, { "id" : "20c8f106-d2fb-422d-9045-22b28151f792", "name" : "basyx-sme-reader-two", @@ -518,6 +526,14 @@ "clientRole" : true, "containerId" : "3fb3e5e5-dbd8-4d51-b964-746c5b2181a4", "attributes" : { } + }, { + "id" : "69f62119-30a1-46cc-b193-b738bae2a4b9", + "name" : "testClientBasyx", + "description" : "", + "composite" : false, + "clientRole" : true, + "containerId" : "3fb3e5e5-dbd8-4d51-b964-746c5b2181a4", + "attributes" : { } }, { "id" : "ba077409-1b5d-4fc8-b20e-10389507fb75", "name" : "basyx-admin", @@ -536,6 +552,14 @@ "attributes" : { } } ], "security-admin-console" : [ ], + "max-sso" : [ { + "id" : "86c919f8-6fd5-4754-9e15-3e113fe2149b", + "name" : "uma_protection", + "composite" : false, + "clientRole" : true, + "containerId" : "b31375f6-1cac-4ba2-a430-6d7e5f9378be", + "attributes" : { } + } ], "admin-cli" : [ ], "account-console" : [ ], "broker" : [ { @@ -624,6 +648,22 @@ } ], "basyx-demo" : [ ], "workstation-1" : [ { + "id" : "ba2b0055-c6c2-4b50-8f3a-947003e7349c", + "name" : "testClientWorkstation", + "description" : "", + "composite" : false, + "clientRole" : true, + "containerId" : "96031210-9e6c-4252-a22e-e81a47e30d65", + "attributes" : { } + }, { + "id" : "dc42731e-3922-4ff0-8084-1c52c20540aa", + "name" : "abc", + "description" : "", + "composite" : false, + "clientRole" : true, + "containerId" : "96031210-9e6c-4252-a22e-e81a47e30d65", + "attributes" : { } + }, { "id" : "914a18c6-4f14-418f-99e0-bfdcf604ac01", "name" : "uma_protection", "composite" : false, @@ -658,7 +698,7 @@ "otpPolicyLookAheadWindow" : 1, "otpPolicyPeriod" : 30, "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ], + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName" ], "webAuthnPolicyRpEntityName" : "keycloak", "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], "webAuthnPolicyRpId" : "", @@ -974,6 +1014,7 @@ "emailVerified" : false, "firstName" : "BaSyx", "lastName" : "Reader", + "email" : "reader@gmail.com", "credentials" : [ { "id" : "eea587f5-439d-487d-85d0-587b226f0683", "type" : "password", @@ -1295,6 +1336,32 @@ "realmRoles" : [ "maintainer", "default-roles-basyx" ], "notBefore" : 0, "groups" : [ ] + }, { + "id" : "c78e370d-4d23-4cb5-854b-9cd966c5fee5", + "createdTimestamp" : 1730127167820, + "username" : "danish", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "56cb59b8-89e1-4423-98f0-dd38a7761366", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1730127228934, + "secretData" : "{\"value\":\"QLQm9eKS9KXF59tim8Im8HnwtpVhrcbe/1+eoY0nVpU=\",\"salt\":\"IzDs16xxmy8wmQIzBNoOHw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-basyx" ], + "clientRoles" : { + "basyx-client-api" : [ "basyx-user" ], + "workstation-1" : [ "testClientWorkstation" ] + }, + "notBefore" : 0, + "groups" : [ ] }, { "id" : "e75ac9e8-7093-4898-a203-d9839f854944", "createdTimestamp" : 1702030567684, @@ -1389,6 +1456,23 @@ }, "notBefore" : 0, "groups" : [ ] + }, { + "id" : "97616c9d-e423-458b-92d0-a7d5832b1d07", + "createdTimestamp" : 1734780525772, + "username" : "service-account-max-sso", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "serviceAccountClientId" : "max-sso", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "admin", "default-roles-basyx" ], + "clientRoles" : { + "max-sso" : [ "uma_protection" ] + }, + "notBefore" : 0, + "groups" : [ ] }, { "id" : "a19abcac-34d5-46bb-a604-b07dc234e80f", "createdTimestamp" : 1715582034760, @@ -1640,6 +1724,133 @@ "nodeReRegistrationTimeout" : 0, "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "b31375f6-1cac-4ba2-a430-6d7e5f9378be", + "clientId" : "max-sso", + "name" : "maximum sso time", + "description" : "This client is created for testing the SSO Max, and also to test the re request the token after expiry. It reduces the access and refresh token expiry time so that it can be tested in the CI.", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "8ccc227xJflxGgtFkwwssHRZUh99nAAc", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : true, + "authorizationServicesEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "access.token.lifespan" : "1", + "client.secret.creation.time" : "1734780525", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false", + "use.refresh.tokens" : "true", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "client_credentials.use_refresh_token" : "false", + "tls.client.certificate.bound.access.tokens" : "false", + "require.pushed.authorization.requests" : "false", + "acr.loa.map" : "{}", + "display.on.consent.screen" : "false", + "client.session.max.lifespan" : "3", + "token.response.type.bearer.lower-case" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "c66dfa3b-0c44-42b0-9398-e975125bbbdd", + "name" : "Client ID", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "client_id", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "client_id", + "jsonType.label" : "String" + } + }, { + "id" : "13ed6cb4-050f-462d-a9fc-c4f6e35f98bd", + "name" : "Client IP Address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientAddress", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientAddress", + "jsonType.label" : "String" + } + }, { + "id" : "b9a2b983-7fb4-47c9-a84f-a3d6ae59bf52", + "name" : "Client Host", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientHost", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientHost", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], + "authorizationSettings" : { + "allowRemoteResourceManagement" : true, + "policyEnforcementMode" : "ENFORCING", + "resources" : [ { + "name" : "Default Resource", + "type" : "urn:max-sso:resources:default", + "ownerManagedAccess" : false, + "attributes" : { }, + "_id" : "11f1c8cd-b49f-4e65-bae4-3f8936c6b0a0", + "uris" : [ "/*" ] + } ], + "policies" : [ { + "id" : "08447d18-a1bd-480b-9fed-083d533de959", + "name" : "Default Policy", + "description" : "A policy that grants access only for users within this realm", + "type" : "regex", + "logic" : "POSITIVE", + "decisionStrategy" : "AFFIRMATIVE", + "config" : { + "code" : "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, { + "id" : "021a6c73-b705-4020-8966-c3bd5492576f", + "name" : "Default Permission", + "description" : "A permission that applies to the default resource type", + "type" : "resource", + "logic" : "POSITIVE", + "decisionStrategy" : "UNANIMOUS", + "config" : { + "defaultResourceType" : "urn:max-sso:resources:default", + "applyPolicies" : "[\"Default Policy\"]" + } + } ], + "scopes" : [ ], + "decisionStrategy" : "UNANIMOUS" + } }, { "id" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", "clientId" : "realm-management", @@ -2301,7 +2512,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "oidc-full-name-mapper", "oidc-usermodel-property-mapper" ] } }, { "id" : "7256d195-1e91-4f63-a9c4-6bef95243a92", @@ -2338,7 +2549,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper" ] } }, { "id" : "face2c9e-4d23-44e2-9a09-74e1d8448bd3", @@ -2364,6 +2575,12 @@ "allow-default-scopes" : [ "true" ] } } ], + "org.keycloak.userprofile.UserProfileProvider" : [ { + "id" : "752a2450-ddf3-4566-9704-a5179390baea", + "providerId" : "declarative-user-profile", + "subComponents" : { }, + "config" : { } + } ], "org.keycloak.keys.KeyProvider" : [ { "id" : "9948a63f-b171-4137-bb81-beabd0c049f0", "name" : "aes-generated",