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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -464,6 +464,11 @@ public class AccountRestService {
|
||||||
return consent;
|
return consent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("/linked-accounts")
|
||||||
|
public LinkedAccountsResource linkedAccounts() {
|
||||||
|
return new LinkedAccountsResource(session, request, client, auth, event, user);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Logs
|
// TODO Logs
|
||||||
|
|
||||||
private static void checkAccountApiEnabled() {
|
private static void checkAccountApiEnabled() {
|
||||||
|
|
|
@ -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">
|
<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>
|
||||||
<div class="col-sm-12 card-pf card-linked-account">
|
<div class="col-sm-12 card-pf card-linked-account">
|
||||||
<div class="card-pf-body row">
|
<div class="card-pf-body row">
|
||||||
|
|
Loading…
Reference in a new issue