KEYCLOAK-5190

This commit is contained in:
Bill Burke 2017-08-07 12:02:18 -04:00
parent cf0ee31bc5
commit c9b7504e3f
6 changed files with 282 additions and 1 deletions

View file

@ -0,0 +1,98 @@
/*
* Copyright 2016 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.bitbucket;
import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.oidc.util.JsonSimpleHttp;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.models.KeycloakSession;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider {
public static final String AUTH_URL = "https://bitbucket.org/site/oauth2/authorize";
public static final String TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token";
public static final String USER_URL = "https://api.bitbucket.org/2.0/user";
public static final String EMAIL_SCOPE = "email";
public static final String ACCOUNT_SCOPE = "account";
public static final String DEFAULT_SCOPE = ACCOUNT_SCOPE;
public BitbucketIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(session, config);
config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL);
String defaultScope = config.getDefaultScope();
if (defaultScope == null || defaultScope.trim().equals("")) {
config.setDefaultScope(ACCOUNT_SCOPE);
}
}
@Override
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
try {
JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(USER_URL, session).header("Authorization", "Bearer " + accessToken));
String type = getJsonProperty(profile, "type");
if (type == null) {
throw new IdentityBrokerException("Could not obtain account information from bitbucket.");
}
if (type.equals("error")) {
JsonNode errorNode = profile.get("error");
if (errorNode != null) {
String errorMsg = getJsonProperty(errorNode, "message");
throw new IdentityBrokerException("Could not obtain account information from bitbucket. Error: " + errorMsg);
} else {
throw new IdentityBrokerException("Could not obtain account information from bitbucket.");
}
}
if (!type.equals("user")) {
logger.debug("Unknown object type: " + type);
throw new IdentityBrokerException("Could not obtain account information from bitbucket.");
}
BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id"));
String username = getJsonProperty(profile, "username");
user.setUsername(username);
user.setIdpConfig(getConfig());
user.setIdp(this);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
} catch (Exception e) {
if (e instanceof IdentityBrokerException) throw (IdentityBrokerException)e;
throw new IdentityBrokerException("Could not obtain user profile from github.", e);
}
}
@Override
protected String getDefaultScopes() {
return DEFAULT_SCOPE;
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2016 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.bitbucket;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
/**
* @author Pedro Igor
*/
public class BitbucketIdentityProviderFactory extends AbstractIdentityProviderFactory<BitbucketIdentityProvider> implements SocialIdentityProviderFactory<BitbucketIdentityProvider> {
public static final String PROVIDER_ID = "bitbucket";
@Override
public String getName() {
return "BitBucket";
}
@Override
public BitbucketIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new BitbucketIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -24,3 +24,4 @@ org.keycloak.social.twitter.TwitterIdentityProviderFactory
org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory
org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory
org.keycloak.social.gitlab.GitLabIdentityProviderFactory
org.keycloak.social.bitbucket.BitbucketIdentityProviderFactory

View file

@ -378,7 +378,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest {
return true;
}
//@Test
@Test
public void testDemo() throws Exception {
testingClient.server().run(FineGrainAdminUnitTest::setupDemo);
Thread.sleep(1000000000);

View file

@ -580,6 +580,12 @@ gitlab.application-id.tooltip=Application Id for the application you created in
gitlab.application-secret.tooltip=Secret for the application that you created in your GitLab Applications account menu
gitlab.default-scopes.tooltip=Scopes to ask for on login. Will always ask for openid. Additionally adds api if you do not specify anything.
bitbucket-consumer-key=Consumer Key
bitbucket-consumer-secret=Consumer Secret
bitbucket.key.tooltip=Bitbucket OAuth Consumer Key
bitbucket.secret.tooltip=Bitbucket OAuth Consumer Secret
bitbucket.default-scopes.tooltip=Scopes to ask for on login. If you do not specify anything, scope defaults to 'email'.
# User federation
sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak
sync-keycloak-roles-to-ldap=Sync Keycloak Roles To LDAP

View file

@ -0,0 +1,130 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
<li data-ng-hide="newIdentityProvider">{{provider.name}}</li>
<li data-ng-show="newIdentityProvider">{{:: 'add-identity-provider' | translate}}</li>
</ol>
<kc-tabs-identity-provider></kc-tabs-identity-provider>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageIdentityProviders">
<input type="text" readonly value="this is not a login form" style="display: none;">
<input type="password" readonly value="this is not a login form" style="display: none;">
<fieldset>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="redirectUri">{{:: 'redirect-uri' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" id="redirectUri" type="text" value="{{callbackUrl}}{{identityProvider.alias}}/endpoint" readonly kc-select-action="click">
</div>
<kc-tooltip>{{:: 'redirect-uri.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<fieldset>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="clientId"><span class="required">*</span> {{:: 'bitbucket-consumer-key' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="clientId" type="text" ng-model="identityProvider.config.clientId" required>
</div>
<kc-tooltip>{{:: 'bitbucket.key.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="clientSecret"><span class="required">*</span> {{:: 'bitbucket-consumer-secret' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="clientSecret" type="password" ng-model="identityProvider.config.clientSecret" required>
</div>
<kc-tooltip>{{:: 'bitbucket.secret.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="defaultScope">{{:: 'default-scopes' | translate}} </label>
<div class="col-md-6">
<input class="form-control" id="defaultScope" type="text" ng-model="identityProvider.config.defaultScope">
</div>
<kc-tooltip>{{:: 'bitbucket.default-scopes.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="enabled">{{:: 'store-tokens' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.storeToken" id="storeToken" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'identity-provider.store-tokens.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="storedTokensReadable">{{:: 'stored-tokens-readable' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.addReadTokenRoleOnCreate" id="storedTokensReadable" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'identity-provider.stored-tokens-readable.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.enabled" id="enabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'identity-provider.enabled.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="trustEmail">{{:: 'trust-email' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.trustEmail" name="identityProvider.trustEmail" id="trustEmail" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="linkOnly">{{:: 'link-only' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.linkOnly" name="identityProvider.trustEmail" id="linkOnly" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'link-only.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="hideOnLoginPage">{{:: 'hide-on-login-page' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.config.hideOnLoginPage" name="identityProvider.config.hideOnLoginPage" id="hideOnLoginPage" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'hide-on-login-page.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="guiOrder">{{:: 'gui-order' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="guiOrder" type="text" ng-model="identityProvider.config.guiOrder">
</div>
<kc-tooltip>{{:: 'gui-order.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="firstBrokerLoginFlowAlias">{{:: 'first-broker-login-flow' | translate}}</label>
<div class="col-md-6">
<div>
<select class="form-control" id="firstBrokerLoginFlowAlias"
ng-model="identityProvider.firstBrokerLoginFlowAlias"
ng-options="flow.alias as flow.alias for flow in authFlows"
required>
</select>
</div>
</div>
<kc-tooltip>{{:: 'first-broker-login-flow.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="postBrokerLoginFlowAlias">{{:: 'post-broker-login-flow' | translate}}</label>
<div class="col-md-6">
<div>
<select class="form-control" id="postBrokerLoginFlowAlias"
ng-model="identityProvider.postBrokerLoginFlowAlias"
ng-options="flow.alias as flow.alias for flow in postBrokerAuthFlows">
</select>
</div>
</div>
<kc-tooltip>{{:: 'post-broker-login-flow.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
<button kc-cancel data-ng-click="cancel()" data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>