Merge pull request #579 from patriot1burke/master
remote token validation
This commit is contained in:
commit
73b7fe6169
7 changed files with 194 additions and 0 deletions
1
audit/api/src/main/java/org/keycloak/audit/Details.java
Normal file → Executable file
1
audit/api/src/main/java/org/keycloak/audit/Details.java
Normal file → Executable file
|
@ -17,6 +17,7 @@ public interface Details {
|
||||||
String REMEMBER_ME = "remember_me";
|
String REMEMBER_ME = "remember_me";
|
||||||
String TOKEN_ID = "token_id";
|
String TOKEN_ID = "token_id";
|
||||||
String REFRESH_TOKEN_ID = "refresh_token_id";
|
String REFRESH_TOKEN_ID = "refresh_token_id";
|
||||||
|
String VALIDATE_ACCESS_TOKEN = "validate_access_token";
|
||||||
String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id";
|
String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
2
audit/api/src/main/java/org/keycloak/audit/EventType.java
Normal file → Executable file
2
audit/api/src/main/java/org/keycloak/audit/EventType.java
Normal file → Executable file
|
@ -14,6 +14,8 @@ public enum EventType {
|
||||||
CODE_TO_TOKEN,
|
CODE_TO_TOKEN,
|
||||||
CODE_TO_TOKEN_ERROR,
|
CODE_TO_TOKEN_ERROR,
|
||||||
REFRESH_TOKEN,
|
REFRESH_TOKEN,
|
||||||
|
VALIDATE_ACCESS_TOKEN,
|
||||||
|
VALIDATE_ACCESS_TOKEN_ERROR,
|
||||||
REFRESH_TOKEN_ERROR,
|
REFRESH_TOKEN_ERROR,
|
||||||
SOCIAL_LINK,
|
SOCIAL_LINK,
|
||||||
SOCIAL_LINK_ERROR,
|
SOCIAL_LINK_ERROR,
|
||||||
|
|
|
@ -10,5 +10,11 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public interface UserFederationProviderFactory extends ProviderFactory<UserFederationProvider> {
|
public interface UserFederationProviderFactory extends ProviderFactory<UserFederationProvider> {
|
||||||
UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model);
|
UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config options to display in generic admin console page for federation
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
List<String> getConfigurationOptions();
|
List<String> getConfigurationOptions();
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.jboss.resteasy.spi.UnauthorizedException;
|
import org.jboss.resteasy.spi.UnauthorizedException;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
|
import org.keycloak.RSATokenVerifier;
|
||||||
|
import org.keycloak.VerificationException;
|
||||||
import org.keycloak.audit.Audit;
|
import org.keycloak.audit.Audit;
|
||||||
import org.keycloak.audit.Details;
|
import org.keycloak.audit.Details;
|
||||||
import org.keycloak.audit.Errors;
|
import org.keycloak.audit.Errors;
|
||||||
|
@ -45,6 +47,7 @@ import org.keycloak.services.resources.flows.Urls;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
import org.keycloak.util.Base64Url;
|
import org.keycloak.util.Base64Url;
|
||||||
import org.keycloak.util.BasicAuthHelper;
|
import org.keycloak.util.BasicAuthHelper;
|
||||||
|
import org.keycloak.util.Time;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
@ -137,6 +140,11 @@ public class TokenService {
|
||||||
return uriBuilder.path(TokenService.class, "accessCodeToToken");
|
return uriBuilder.path(TokenService.class, "accessCodeToToken");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static UriBuilder validateAccessTokenUrl(UriBuilder baseUriBuilder) {
|
||||||
|
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
|
||||||
|
return uriBuilder.path(TokenService.class, "validateAccessToken");
|
||||||
|
}
|
||||||
|
|
||||||
public static UriBuilder grantAccessTokenUrl(UriInfo uriInfo) {
|
public static UriBuilder grantAccessTokenUrl(UriInfo uriInfo) {
|
||||||
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
|
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
|
||||||
return grantAccessTokenUrl(baseUriBuilder);
|
return grantAccessTokenUrl(baseUriBuilder);
|
||||||
|
@ -295,6 +303,105 @@ public class TokenService {
|
||||||
return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build();
|
return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate encoded access token.
|
||||||
|
*
|
||||||
|
* @param tokenString
|
||||||
|
* @return Unmarshalled token
|
||||||
|
*/
|
||||||
|
@Path("validate")
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Response validateAccessToken(@QueryParam("access_token") String tokenString) {
|
||||||
|
audit.event(EventType.VALIDATE_ACCESS_TOKEN);
|
||||||
|
AccessToken token = null;
|
||||||
|
try {
|
||||||
|
token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Map<String, String> err = new HashMap<String, String>();
|
||||||
|
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||||
|
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Token invalid");
|
||||||
|
audit.error(Errors.INVALID_TOKEN);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
audit.user(token.getSubject()).session(token.getSessionState()).detail(Details.VALIDATE_ACCESS_TOKEN, token.getId());
|
||||||
|
|
||||||
|
if (token.isExpired()
|
||||||
|
|| token.getIssuedAt() < realm.getNotBefore()
|
||||||
|
) {
|
||||||
|
Map<String, String> err = new HashMap<String, String>();
|
||||||
|
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||||
|
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Token expired");
|
||||||
|
audit.error(Errors.INVALID_TOKEN);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
UserModel user = session.users().getUserById(token.getSubject(), realm);
|
||||||
|
if (user == null) {
|
||||||
|
Map<String, String> err = new HashMap<String, String>();
|
||||||
|
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||||
|
err.put(OAuth2Constants.ERROR_DESCRIPTION, "User does not exist");
|
||||||
|
audit.error(Errors.USER_NOT_FOUND);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isEnabled()) {
|
||||||
|
Map<String, String> err = new HashMap<String, String>();
|
||||||
|
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||||
|
err.put(OAuth2Constants.ERROR_DESCRIPTION, "User disabled");
|
||||||
|
audit.error(Errors.USER_DISABLED);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
|
||||||
|
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||||
|
Map<String, String> err = new HashMap<String, String>();
|
||||||
|
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
|
||||||
|
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Expired session");
|
||||||
|
audit.error(Errors.USER_SESSION_NOT_FOUND);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientModel client = realm.findClient(token.getIssuedFor());
|
||||||
|
if (client == null) {
|
||||||
|
Map<String, String> err = new HashMap<String, String>();
|
||||||
|
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_CLIENT);
|
||||||
|
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Issued for client no longer exists");
|
||||||
|
audit.error(Errors.CLIENT_NOT_FOUND);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.getIssuedAt() < client.getNotBefore()) {
|
||||||
|
Map<String, String> err = new HashMap<String, String>();
|
||||||
|
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_CLIENT);
|
||||||
|
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Issued for client no longer exists");
|
||||||
|
audit.error(Errors.INVALID_TOKEN);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
tokenManager.verifyAccess(token, realm, client, user);
|
||||||
|
} catch (OAuthErrorException e) {
|
||||||
|
Map<String, String> err = new HashMap<String, String>();
|
||||||
|
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_SCOPE);
|
||||||
|
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Role mappings have changed");
|
||||||
|
audit.error(Errors.INVALID_TOKEN);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
return Response.ok(token, MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL for making refresh token requests.
|
* URL for making refresh token requests.
|
||||||
*
|
*
|
||||||
|
|
|
@ -365,4 +365,6 @@ public class AdapterTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
|
import org.keycloak.services.resources.TokenService;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.OAuthClient;
|
import org.keycloak.testsuite.OAuthClient;
|
||||||
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
|
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
|
||||||
|
@ -41,8 +42,22 @@ import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
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.BasicAuthHelper;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
|
import javax.ws.rs.client.Client;
|
||||||
|
import javax.ws.rs.client.ClientBuilder;
|
||||||
|
import javax.ws.rs.client.Entity;
|
||||||
|
import javax.ws.rs.client.WebTarget;
|
||||||
|
import javax.ws.rs.core.Form;
|
||||||
|
import javax.ws.rs.core.GenericType;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.allOf;
|
import static org.hamcrest.Matchers.allOf;
|
||||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||||
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
||||||
|
@ -108,6 +123,7 @@ public class AccessTokenTest {
|
||||||
Assert.assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
|
Assert.assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
|
||||||
Assert.assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
|
Assert.assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
|
||||||
Assert.assertEquals(sessionId, token.getSessionState());
|
Assert.assertEquals(sessionId, token.getSessionState());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -247,4 +263,63 @@ public class AccessTokenTest {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateAccessToken() throws Exception {
|
||||||
|
Client client = ClientBuilder.newClient();
|
||||||
|
UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
|
||||||
|
URI grantUri = TokenService.grantAccessTokenUrl(builder).build("test");
|
||||||
|
WebTarget grantTarget = client.target(grantUri);
|
||||||
|
builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
|
||||||
|
URI validateUri = TokenService.validateAccessTokenUrl(builder).build("test");
|
||||||
|
WebTarget validateTarget = client.target(validateUri);
|
||||||
|
|
||||||
|
{
|
||||||
|
Response response = validateTarget.queryParam("access_token", "bad token").request().get();
|
||||||
|
Assert.assertEquals(400, response.getStatus());
|
||||||
|
HashMap<String, String> error = response.readEntity(new GenericType <HashMap<String, String>>() {});
|
||||||
|
Assert.assertNotNull(error.get("error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
org.keycloak.representations.AccessTokenResponse tokenResponse = null;
|
||||||
|
{
|
||||||
|
String header = BasicAuthHelper.createHeader("test-app", "password");
|
||||||
|
Form form = new Form();
|
||||||
|
form.param("username", "test-user@localhost")
|
||||||
|
.param("password", "password");
|
||||||
|
Response response = grantTarget.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, header)
|
||||||
|
.post(Entity.form(form));
|
||||||
|
Assert.assertEquals(200, response.getStatus());
|
||||||
|
tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Response response = validateTarget.queryParam("access_token", tokenResponse.getToken()).request().get();
|
||||||
|
Assert.assertEquals(200, response.getStatus());
|
||||||
|
AccessToken token = response.readEntity(AccessToken.class);
|
||||||
|
Assert.assertNotNull(token);
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
|
||||||
|
URI logoutUri = TokenService.logoutUrl(builder).build("test");
|
||||||
|
Response response = client.target(logoutUri).queryParam("session_state", tokenResponse.getSessionState()).request().get();
|
||||||
|
Assert.assertEquals(200, response.getStatus());
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Response response = validateTarget.queryParam("access_token", tokenResponse.getToken()).request().get();
|
||||||
|
Assert.assertEquals(400, response.getStatus());
|
||||||
|
HashMap<String, String> error = response.readEntity(new GenericType <HashMap<String, String>>() {});
|
||||||
|
Assert.assertNotNull(error.get("error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"sslRequired": "external",
|
"sslRequired": "external",
|
||||||
"registrationAllowed": true,
|
"registrationAllowed": true,
|
||||||
"resetPasswordAllowed": true,
|
"resetPasswordAllowed": true,
|
||||||
|
"passwordCredentialGrantAllowed": true,
|
||||||
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
||||||
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||||
"requiredCredentials": [ "password" ],
|
"requiredCredentials": [ "password" ],
|
||||||
|
|
Loading…
Reference in a new issue