From f11573eeb26f5a3c47374d9f01a2f6616b7ca82b Mon Sep 17 00:00:00 2001 From: Neon Ngo Date: Wed, 6 Apr 2022 07:45:11 -0400 Subject: [PATCH] KEYCLOAK-13828 Allow override of baseUrl and apiUrl in GitHub identity provider (#7021) Allow override of baseUrl & apiUrl in GitHub identity provider Closes #11144 --- .../social/github/GitHubIdentityProvider.java | 91 +++++++++++++++---- .../github/GitHubIdentityProviderTest.java | 67 ++++++++++++++ .../messages/admin-messages_en.properties | 4 + .../realm-identity-provider-github-ext.html | 14 +++ 4 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 services/src/test/java/org/keycloak/social/github/GitHubIdentityProviderTest.java diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java index fff4a4093b..d612ed01c4 100755 --- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java @@ -35,18 +35,79 @@ import org.keycloak.models.KeycloakSession; */ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider { - public static final String AUTH_URL = "https://github.com/login/oauth/authorize"; - public static final String TOKEN_URL = "https://github.com/login/oauth/access_token"; - public static final String PROFILE_URL = "https://api.github.com/user"; - public static final String EMAIL_URL = "https://api.github.com/user/emails"; - public static final String DEFAULT_SCOPE = "user:email"; + public static final String DEFAULT_BASE_URL = "https://github.com"; + public static final String AUTH_FRAGMENT = "/login/oauth/authorize"; + public static final String TOKEN_FRAGMENT = "/login/oauth/access_token"; + public static final String DEFAULT_AUTH_URL = DEFAULT_BASE_URL + AUTH_FRAGMENT; + public static final String DEFAULT_TOKEN_URL = DEFAULT_BASE_URL + TOKEN_FRAGMENT; + /** @deprecated Use {@link #DEFAULT_AUTH_URL} instead. */ + @Deprecated + public static final String AUTH_URL = DEFAULT_AUTH_URL; + /** @deprecated Use {@link #DEFAULT_TOKEN_URL} instead. */ + @Deprecated + public static final String TOKEN_URL = DEFAULT_TOKEN_URL; - public GitHubIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { - super(session, config); - config.setAuthorizationUrl(AUTH_URL); - config.setTokenUrl(TOKEN_URL); - config.setUserInfoUrl(PROFILE_URL); - } + public static final String DEFAULT_API_URL = "https://api.github.com"; + public static final String PROFILE_FRAGMENT = "/user"; + public static final String EMAIL_FRAGMENT = "/user/emails"; + public static final String DEFAULT_PROFILE_URL = DEFAULT_API_URL + PROFILE_FRAGMENT; + public static final String DEFAULT_EMAIL_URL = DEFAULT_API_URL + EMAIL_FRAGMENT; + /** @deprecated Use {@link #DEFAULT_PROFILE_URL} instead. */ + @Deprecated + public static final String PROFILE_URL = DEFAULT_PROFILE_URL; + /** @deprecated Use {@link #DEFAULT_EMAIL_URL} instead. */ + @Deprecated + public static final String EMAIL_URL = DEFAULT_EMAIL_URL; + + public static final String DEFAULT_SCOPE = "user:email"; + + /** Base URL key in config map. */ + protected static final String BASE_URL_KEY = "baseUrl"; + /** API URL key in config map. */ + protected static final String API_URL_KEY = "apiUrl"; + /** Email URL key in config map. */ + protected static final String EMAIL_URL_KEY = "emailUrl"; + + private final String authUrl; + private final String tokenUrl; + private final String profileUrl; + private final String emailUrl; + + public GitHubIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { + super(session, config); + + String baseUrl = getUrlFromConfig(config, BASE_URL_KEY, DEFAULT_BASE_URL); + String apiUrl = getUrlFromConfig(config, API_URL_KEY, DEFAULT_API_URL); + + authUrl = baseUrl + AUTH_FRAGMENT; + tokenUrl = baseUrl + TOKEN_FRAGMENT; + profileUrl = apiUrl + PROFILE_FRAGMENT; + emailUrl = apiUrl + EMAIL_FRAGMENT; + + config.setAuthorizationUrl(authUrl); + config.setTokenUrl(tokenUrl); + config.setUserInfoUrl(profileUrl); + config.getConfig().put(EMAIL_URL_KEY, emailUrl); + } + + /** + * Get URL from config with default value fallback. + * + * @param config Identity provider configuration. + * @param key Key to look for value in config's config map. + * @param defaultValue Default value if value at key is null or empty string. + * @return URL for specified key in the configuration with default value fallback. + */ + protected static String getUrlFromConfig(OAuth2IdentityProviderConfig config, String key, String defaultValue) { + String url = config.getConfig().get(key); + if (url == null || url.trim().isEmpty()) { + url = defaultValue; + } + if (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + return url; + } @Override protected boolean supportsExternalExchange() { @@ -55,7 +116,7 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple @Override protected String getProfileEndpointForValidation(EventBuilder event) { - return PROFILE_URL; + return profileUrl; } @Override @@ -72,14 +133,12 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); return user; - } - @Override protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { try { - JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson(); + JsonNode profile = SimpleHttp.doGet(profileUrl, session).header("Authorization", "Bearer " + accessToken).asJson(); BrokeredIdentityContext user = extractIdentityFromProfile(null, profile); @@ -95,7 +154,7 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple private String searchEmail(String accessToken) { try { - ArrayNode emails = (ArrayNode) SimpleHttp.doGet(EMAIL_URL, session).header("Authorization", "Bearer " + accessToken).asJson(); + ArrayNode emails = (ArrayNode) SimpleHttp.doGet(emailUrl, session).header("Authorization", "Bearer " + accessToken).asJson(); Iterator loop = emails.elements(); while (loop.hasNext()) { diff --git a/services/src/test/java/org/keycloak/social/github/GitHubIdentityProviderTest.java b/services/src/test/java/org/keycloak/social/github/GitHubIdentityProviderTest.java new file mode 100644 index 0000000000..e059820c9d --- /dev/null +++ b/services/src/test/java/org/keycloak/social/github/GitHubIdentityProviderTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 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.social.github; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; + +/** + * Unit test for {@link org.keycloak.social.github.GitHubIdentityProvider}. + * + * @author Neon Ngo + */ +public class GitHubIdentityProviderTest { + + /** + * Test constructor with empty config (i.e. to use default values). + * This also tests GitHubIdentityProvider.getProfileEndpointForValidation(null). + */ + @Test + public void testGitHubIdentityProvider() { + OAuth2IdentityProviderConfig config = new OAuth2IdentityProviderConfig(); + GitHubIdentityProvider idp = new GitHubIdentityProvider(null, config); + + validateUrls(idp, GitHubIdentityProvider.DEFAULT_BASE_URL, GitHubIdentityProvider.DEFAULT_API_URL); + } + + /** + * Test constructor with config overrides of default base URL and API URL. + */ + @Test + public void testGitHubIdentityProviderOverrides() { + OAuth2IdentityProviderConfig config = new OAuth2IdentityProviderConfig(); + String baseUrl = "https://test.com"; + String apiUrl = "https://api.test.com"; + config.getConfig().put(GitHubIdentityProvider.BASE_URL_KEY, baseUrl); + config.getConfig().put(GitHubIdentityProvider.API_URL_KEY, apiUrl); + GitHubIdentityProvider idp = new GitHubIdentityProvider(null, config); + + validateUrls(idp, baseUrl, apiUrl); + } + + protected void validateUrls(GitHubIdentityProvider idp, String baseUrl, String apiUrl) { + OAuth2IdentityProviderConfig config = idp.getConfig(); + assertEquals(baseUrl + GitHubIdentityProvider.AUTH_FRAGMENT, config.getAuthorizationUrl()); + assertEquals(baseUrl + GitHubIdentityProvider.TOKEN_FRAGMENT, config.getTokenUrl()); + assertEquals(apiUrl + GitHubIdentityProvider.EMAIL_FRAGMENT, config.getConfig().get(GitHubIdentityProvider.EMAIL_URL_KEY)); + assertEquals(apiUrl + GitHubIdentityProvider.PROFILE_FRAGMENT, config.getUserInfoUrl()); + assertEquals(apiUrl + GitHubIdentityProvider.PROFILE_FRAGMENT, idp.getProfileEndpointForValidation(null)); + } + +} diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 3ead1891e2..876c88cf0a 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -668,6 +668,10 @@ redirect-uri.tooltip=The redirect uri to use when configuring the identity provi alias=Alias display-name=Display Name identity-provider.alias.tooltip=The alias uniquely identifies an identity provider and it is also used to build the redirect uri. +identity-provider.api-url=API URL +identity-provider.api-url.tooltip=Override the default API URL for this identity provider. +identity-provider.base-url=Base URL +identity-provider.base-url.tooltip=Override the default Base URL for this identity provider. identity-provider.display-name.tooltip=Friendly name for Identity Providers. identity-provider.enabled.tooltip=Enable/disable this identity provider. authenticate-by-default=Authenticate by Default diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-github-ext.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-github-ext.html index e69de29bb2..8c9e30feb8 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-github-ext.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-github-ext.html @@ -0,0 +1,14 @@ +
+ +
+ +
+ {{:: 'identity-provider.base-url.tooltip' | translate}} +
+
+ +
+ +
+ {{:: 'identity-provider.api-url.tooltip' | translate}} +
\ No newline at end of file