KEYCLOAK-7429: Linked Accounts REST API
This commit is contained in:
parent
d0386dab85
commit
041229f9ca
6 changed files with 617 additions and 1 deletions
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.representations.account;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
public class AccountLinkUriRepresentation {
|
||||
private URI accountLinkUri;
|
||||
private String nonce;
|
||||
private String hash;
|
||||
|
||||
public URI getAccountLinkUri() {
|
||||
return accountLinkUri;
|
||||
}
|
||||
|
||||
public void setAccountLinkUri(URI accountLinkUri) {
|
||||
this.accountLinkUri = accountLinkUri;
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public void setNonce(String nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public void setHash(String hash) {
|
||||
this.hash = hash;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.representations.account;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
public class LinkedAccountRepresentation implements Comparable<LinkedAccountRepresentation> {
|
||||
private boolean connected;
|
||||
private String providerAlias;
|
||||
private String providerName;
|
||||
private String displayName;
|
||||
private String linkedUsername;
|
||||
|
||||
@JsonIgnore
|
||||
private String guiOrder;
|
||||
|
||||
public String getLinkedUsername() {
|
||||
return linkedUsername;
|
||||
}
|
||||
|
||||
public void setLinkedUsername(String userName) {
|
||||
this.linkedUsername = userName;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
public void setConnected(boolean connected) {
|
||||
this.connected = connected;
|
||||
}
|
||||
|
||||
public String getProviderAlias() {
|
||||
return providerAlias;
|
||||
}
|
||||
|
||||
public void setProviderAlias(String providerId) {
|
||||
this.providerAlias = providerId;
|
||||
}
|
||||
|
||||
public String getProviderName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
public void setProviderName(String providerName) {
|
||||
this.providerName = providerName;
|
||||
}
|
||||
|
||||
public String getGuiOrder() {
|
||||
return guiOrder;
|
||||
}
|
||||
|
||||
public void setGuiOrder(String guiOrder) {
|
||||
this.guiOrder = guiOrder;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(LinkedAccountRepresentation rep) {
|
||||
if (this.getGuiOrder() == null) return -1;
|
||||
if (rep.getGuiOrder() == null) return 1;
|
||||
|
||||
return rep.getGuiOrder().compareTo(this.getGuiOrder());
|
||||
}
|
||||
|
||||
}
|
|
@ -463,6 +463,11 @@ public class AccountRestService {
|
|||
}
|
||||
return consent;
|
||||
}
|
||||
|
||||
@Path("/linked-accounts")
|
||||
public LinkedAccountsResource linkedAccounts() {
|
||||
return new LinkedAccountsResource(session, request, client, auth, event, user);
|
||||
}
|
||||
|
||||
// TODO Logs
|
||||
|
||||
|
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.services.resources.account;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.account.AccountLinkUriRepresentation;
|
||||
import org.keycloak.representations.account.LinkedAccountRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
|
||||
/**
|
||||
* API for linking/unlinking social login accounts
|
||||
*
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
public class LinkedAccountsResource {
|
||||
private static final Logger logger = Logger.getLogger(LinkedAccountsResource.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final HttpRequest request;
|
||||
private final ClientModel client;
|
||||
private final EventBuilder event;
|
||||
private final UserModel user;
|
||||
private final RealmModel realm;
|
||||
private final Auth auth;
|
||||
|
||||
public LinkedAccountsResource(KeycloakSession session,
|
||||
HttpRequest request,
|
||||
ClientModel client,
|
||||
Auth auth,
|
||||
EventBuilder event,
|
||||
UserModel user) {
|
||||
this.session = session;
|
||||
this.request = request;
|
||||
this.client = client;
|
||||
this.auth = auth;
|
||||
this.event = event;
|
||||
this.user = user;
|
||||
realm = session.getContext().getRealm();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response linkedAccounts() {
|
||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
SortedSet<LinkedAccountRepresentation> linkedAccounts = getLinkedAccounts(this.session, this.realm, this.user);
|
||||
return Cors.add(request, Response.ok(linkedAccounts)).auth().allowedOrigins(auth.getToken()).build();
|
||||
}
|
||||
|
||||
public SortedSet<LinkedAccountRepresentation> getLinkedAccounts(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
|
||||
SortedSet<LinkedAccountRepresentation> linkedAccounts = new TreeSet<>();
|
||||
|
||||
if (identityProviders == null || identityProviders.isEmpty()) return linkedAccounts;
|
||||
|
||||
Set<FederatedIdentityModel> identities = session.users().getFederatedIdentities(user, realm);
|
||||
for (IdentityProviderModel provider : identityProviders) {
|
||||
if (!provider.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
String providerId = provider.getAlias();
|
||||
|
||||
FederatedIdentityModel identity = getIdentity(identities, providerId);
|
||||
|
||||
String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, provider);
|
||||
String guiOrder = provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null;
|
||||
|
||||
LinkedAccountRepresentation rep = new LinkedAccountRepresentation();
|
||||
rep.setConnected(identity != null);
|
||||
rep.setProviderAlias(providerId);
|
||||
rep.setDisplayName(displayName);
|
||||
rep.setGuiOrder(guiOrder);
|
||||
rep.setProviderName(provider.getAlias());
|
||||
if (identity != null) {
|
||||
rep.setLinkedUsername(identity.getUserName());
|
||||
}
|
||||
linkedAccounts.add(rep);
|
||||
}
|
||||
|
||||
return linkedAccounts;
|
||||
}
|
||||
|
||||
private FederatedIdentityModel getIdentity(Set<FederatedIdentityModel> identities, String providerId) {
|
||||
for (FederatedIdentityModel link : identities) {
|
||||
if (providerId.equals(link.getIdentityProvider())) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{providerId}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Deprecated
|
||||
public Response buildLinkedAccountURI(@PathParam("providerId") String providerId,
|
||||
@QueryParam("redirectUri") String redirectUri) {
|
||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||
|
||||
if (redirectUri == null) {
|
||||
ErrorResponse.error(Messages.INVALID_REDIRECT_URI, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
String errorMessage = checkCommonPreconditions(providerId);
|
||||
if (errorMessage != null) {
|
||||
return ErrorResponse.error(errorMessage, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
String nonce = UUID.randomUUID().toString();
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
String input = nonce + auth.getSession().getId() + client.getClientId() + providerId;
|
||||
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
String hash = Base64Url.encode(check);
|
||||
URI linkUri = Urls.identityProviderLinkRequest(this.session.getContext().getUri().getBaseUri(), providerId, realm.getName());
|
||||
linkUri = UriBuilder.fromUri(linkUri)
|
||||
.queryParam("nonce", nonce)
|
||||
.queryParam("hash", hash)
|
||||
.queryParam("client_id", client.getClientId())
|
||||
.queryParam("redirect_uri", redirectUri)
|
||||
.build();
|
||||
|
||||
AccountLinkUriRepresentation rep = new AccountLinkUriRepresentation();
|
||||
rep.setAccountLinkUri(linkUri);
|
||||
rep.setHash(hash);
|
||||
rep.setNonce(nonce);
|
||||
|
||||
return Cors.add(request, Response.ok(rep)).auth().allowedOrigins(auth.getToken()).build();
|
||||
} catch (Exception spe) {
|
||||
spe.printStackTrace();
|
||||
return ErrorResponse.error(Messages.FAILED_TO_PROCESS_RESPONSE, Response.Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{providerId}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response removeLinkedAccount(@PathParam("providerId") String providerId) {
|
||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||
|
||||
String errorMessage = checkCommonPreconditions(providerId);
|
||||
if (errorMessage != null) {
|
||||
return ErrorResponse.error(errorMessage, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
FederatedIdentityModel link = session.users().getFederatedIdentity(user, providerId, realm);
|
||||
if (link == null) {
|
||||
return ErrorResponse.error(Messages.FEDERATED_IDENTITY_NOT_ACTIVE, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Removing last social provider is not possible if you don't have other possibility to authenticate
|
||||
if (!(session.users().getFederatedIdentities(user, realm).size() > 1 || user.getFederationLink() != null || isPasswordSet())) {
|
||||
return ErrorResponse.error(Messages.FEDERATED_IDENTITY_REMOVING_LAST_PROVIDER, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
session.users().removeFederatedIdentity(realm, user, providerId);
|
||||
|
||||
logger.debugv("Social provider {0} removed successfully from user {1}", providerId, user.getUsername());
|
||||
|
||||
event.event(EventType.REMOVE_FEDERATED_IDENTITY).client(auth.getClient()).user(auth.getUser())
|
||||
.detail(Details.USERNAME, auth.getUser().getUsername())
|
||||
.detail(Details.IDENTITY_PROVIDER, link.getIdentityProvider())
|
||||
.detail(Details.IDENTITY_PROVIDER_USERNAME, link.getUserName())
|
||||
.success();
|
||||
|
||||
return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build();
|
||||
}
|
||||
|
||||
private String checkCommonPreconditions(String providerId) {
|
||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||
|
||||
if (Validation.isEmpty(providerId)) {
|
||||
return Messages.MISSING_IDENTITY_PROVIDER;
|
||||
}
|
||||
|
||||
if (!isValidProvider(providerId)) {
|
||||
return Messages.IDENTITY_PROVIDER_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (!user.isEnabled()) {
|
||||
return Messages.ACCOUNT_DISABLED;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isPasswordSet() {
|
||||
return session.userCredentialManager().isConfiguredFor(realm, user, CredentialModel.PASSWORD);
|
||||
}
|
||||
|
||||
private boolean isValidProvider(String providerId) {
|
||||
for (IdentityProviderModel model : realm.getIdentityProviders()) {
|
||||
if (model.getAlias().equals(providerId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.account;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.util.TokenUtil;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.SortedSet;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.keycloak.representations.account.AccountLinkUriRepresentation;
|
||||
import org.keycloak.representations.account.LinkedAccountRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:ssilvert@redhat.com">Stan Silvert</a>
|
||||
*/
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class LinkedAccountsRestServiceTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public TokenUtil tokenUtil = new TokenUtil();
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
private CloseableHttpClient client;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
client = HttpClientBuilder.create().build();
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
try {
|
||||
client.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
testRealm.getUsers().add(UserBuilder.create().username("no-account-access").password("password").build());
|
||||
testRealm.getUsers().add(UserBuilder.create().username("view-account-access").role("account", "view-profile").password("password").build());
|
||||
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
.providerId("github")
|
||||
.alias("github")
|
||||
.setAttribute("guiOrder", "2")
|
||||
.build());
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
.providerId("saml")
|
||||
.alias("mysaml")
|
||||
.setAttribute("guiOrder", "0")
|
||||
.build());
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
.providerId("oidc")
|
||||
.alias("myoidc")
|
||||
.displayName("MyOIDC")
|
||||
.setAttribute("guiOrder", "1")
|
||||
.build());
|
||||
|
||||
addGitHubIdentity(testRealm);
|
||||
}
|
||||
|
||||
private void addGitHubIdentity(RealmRepresentation testRealm) {
|
||||
UserRepresentation acctMgtUser = findUser(testRealm, "test-user@localhost");
|
||||
|
||||
FederatedIdentityRepresentation fedIdp = new FederatedIdentityRepresentation();
|
||||
fedIdp.setIdentityProvider("github");
|
||||
fedIdp.setUserId("foo");
|
||||
fedIdp.setUserName("foo");
|
||||
|
||||
ArrayList<FederatedIdentityRepresentation> fedIdps = new ArrayList<>();
|
||||
fedIdps.add(fedIdp);
|
||||
|
||||
acctMgtUser.setFederatedIdentities(fedIdps);
|
||||
}
|
||||
|
||||
private UserRepresentation findUser(RealmRepresentation testRealm, String userName) {
|
||||
for (UserRepresentation user : testRealm.getUsers()) {
|
||||
if (user.getUsername().equals(userName)) return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getAccountUrl(String resource) {
|
||||
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : "");
|
||||
}
|
||||
|
||||
private SortedSet<LinkedAccountRepresentation> linkedAccountsRep() throws IOException {
|
||||
return SimpleHttp.doGet(getAccountUrl("linked-accounts"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<SortedSet<LinkedAccountRepresentation>>() {});
|
||||
}
|
||||
|
||||
private LinkedAccountRepresentation findLinkedAccount(String providerAlias) throws IOException {
|
||||
for (LinkedAccountRepresentation account : linkedAccountsRep()) {
|
||||
if (account.getProviderAlias().equals(providerAlias)) return account;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildLinkedAccountUri() throws IOException {
|
||||
AccountLinkUriRepresentation rep = SimpleHttp.doGet(getAccountUrl("linked-accounts/github?redirectUri=phonyUri"), client)
|
||||
.auth(tokenUtil.getToken())
|
||||
.asJson(new TypeReference<AccountLinkUriRepresentation>() {});
|
||||
URI brokerUri = rep.getAccountLinkUri();
|
||||
|
||||
assertTrue(brokerUri.getPath().endsWith("/auth/realms/test/broker/github/link"));
|
||||
|
||||
List<NameValuePair> queryParams = URLEncodedUtils.parse(brokerUri, Charset.defaultCharset());
|
||||
assertEquals(4, queryParams.size());
|
||||
for (NameValuePair nvp : queryParams) {
|
||||
switch (nvp.getName()) {
|
||||
case "nonce" : {
|
||||
assertNotNull(nvp.getValue());
|
||||
assertEquals(rep.getNonce(), nvp.getValue());
|
||||
break;
|
||||
}
|
||||
case "hash" : {
|
||||
assertNotNull(nvp.getValue());
|
||||
assertEquals(rep.getHash(), nvp.getValue());
|
||||
break;
|
||||
}
|
||||
case "client_id" : assertEquals("account", nvp.getValue()); break;
|
||||
case "redirect_uri" : assertEquals("phonyUri", nvp.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLinkedAccounts() throws IOException {
|
||||
SortedSet<LinkedAccountRepresentation> details = linkedAccountsRep();
|
||||
assertEquals(3, details.size());
|
||||
|
||||
int order = 0;
|
||||
for (LinkedAccountRepresentation account : details) {
|
||||
if (account.getProviderAlias().equals("github")) {
|
||||
assertTrue(account.isConnected());
|
||||
} else {
|
||||
assertFalse(account.isConnected());
|
||||
}
|
||||
|
||||
// test that accounts were sorted by guiOrder
|
||||
if (order == 0) assertEquals("mysaml", account.getDisplayName());
|
||||
if (order == 1) assertEquals("MyOIDC", account.getDisplayName());
|
||||
if (order == 2) assertEquals("GitHub", account.getDisplayName());
|
||||
order++;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveLinkedAccount() throws IOException {
|
||||
assertTrue(findLinkedAccount("github").isConnected());
|
||||
SimpleHttp.doDelete(getAccountUrl("linked-accounts/github"), client).auth(tokenUtil.getToken()).acceptJson().asResponse();
|
||||
assertFalse(findLinkedAccount("github").isConnected());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,23 @@
|
|||
<div class="page-header">
|
||||
<h1 id="pageTitle">{{'linkedAccountsHtmlTitle' | translate}}</h1>
|
||||
<h1>{{'linkedAccountsHtmlTitle' | translate}}</h1>
|
||||
<hr/>
|
||||
<h2>Buttons for manual testing. See JS Console for output.</h2>
|
||||
<div class="card-pf-body row">
|
||||
<button class="label label-primary" (click)="doGet()">List All</button>
|
||||
</div>
|
||||
<div class="card-pf-body row">
|
||||
<button class="label label-primary" (click)="linkAccount('github')">Link Account to GitHub</button>
|
||||
<button class="label label-primary" (click)="doRemove('github')">Remove GitHub</button>
|
||||
</div>
|
||||
<div class="card-pf-body row">
|
||||
<button class="label label-primary" (click)="linkAccount('stackoverflow')">Link Account to Stack Overflow</button>
|
||||
<button class="label label-primary" (click)="doRemove('stackoverflow')">Remove Stack Overflow</button>
|
||||
</div>
|
||||
<div class="card-pf-body row">
|
||||
<button class="label label-primary" (click)="linkAccount('linkedin')">Link Account to LinkedIn</button>
|
||||
<button class="label label-primary" (click)="doRemove('linkedin')">Remove LinkedIn</button>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
<div class="col-sm-12 card-pf card-linked-account">
|
||||
<div class="card-pf-body row">
|
||||
|
|
Loading…
Reference in a new issue