commit
3fce14d9ce
6 changed files with 344 additions and 43 deletions
|
@ -75,7 +75,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
String defaultScope = config.getDefaultScope();
|
||||
|
||||
if (!defaultScope.contains(SCOPE_OPENID)) {
|
||||
config.setDefaultScope(SCOPE_OPENID + " " + defaultScope);
|
||||
config.setDefaultScope((SCOPE_OPENID + " " + defaultScope).trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,10 +232,23 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
JsonWebToken idToken = validateToken(encodedIdToken);
|
||||
|
||||
try {
|
||||
BrokeredIdentityContext identity = extractIdentity(tokenResponse, accessToken, idToken);
|
||||
|
||||
if (getConfig().isStoreToken()) {
|
||||
identity.setToken(response);
|
||||
}
|
||||
|
||||
return identity;
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not fetch attributes from userinfo endpoint.", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
|
||||
String id = idToken.getSubject();
|
||||
BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
|
||||
String name = (String)idToken.getOtherClaims().get(IDToken.NAME);
|
||||
String preferredUsername = (String)idToken.getOtherClaims().get(IDToken.PREFERRED_USERNAME);
|
||||
String preferredUsername = (String)idToken.getOtherClaims().get(getUsernameClaimName());
|
||||
String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
|
||||
|
||||
if (!getConfig().isDisableUserInfoService()) {
|
||||
|
@ -274,15 +287,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
}
|
||||
|
||||
identity.setUsername(preferredUsername);
|
||||
|
||||
if (getConfig().isStoreToken()) {
|
||||
identity.setToken(response);
|
||||
}
|
||||
|
||||
return identity;
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not fetch attributes from userinfo endpoint.", e);
|
||||
}
|
||||
|
||||
protected String getUsernameClaimName() {
|
||||
return IDToken.PREFERRED_USERNAME;
|
||||
}
|
||||
|
||||
protected String getUserInfoUrl() {
|
||||
|
|
109
services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
Executable file
109
services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
Executable file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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.gitlab;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
|
||||
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
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;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class GitLabIdentityProvider extends OIDCIdentityProvider implements SocialIdentityProvider<OIDCIdentityProviderConfig> {
|
||||
|
||||
public static final String AUTH_URL = "https://gitlab.com/oauth/authorize";
|
||||
public static final String TOKEN_URL = "https://gitlab.com/oauth/token";
|
||||
public static final String USER_INFO = "https://gitlab.com/api/v4/user";
|
||||
public static final String API_SCOPE = "api";
|
||||
|
||||
public GitLabIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||
super(session, config);
|
||||
config.setAuthorizationUrl(AUTH_URL);
|
||||
config.setTokenUrl(TOKEN_URL);
|
||||
config.setUserInfoUrl(USER_INFO);
|
||||
|
||||
String defaultScope = config.getDefaultScope();
|
||||
|
||||
if (defaultScope.equals(SCOPE_OPENID)) {
|
||||
config.setDefaultScope((API_SCOPE + " " + defaultScope).trim());
|
||||
}
|
||||
}
|
||||
|
||||
protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
|
||||
String id = idToken.getSubject();
|
||||
BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
|
||||
String name = (String)idToken.getOtherClaims().get(IDToken.NAME);
|
||||
String preferredUsername = (String)idToken.getOtherClaims().get(IDToken.NICKNAME);
|
||||
String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
|
||||
|
||||
if (getConfig().getDefaultScope().contains(API_SCOPE)) {
|
||||
String userInfoUrl = getUserInfoUrl();
|
||||
if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) {
|
||||
SimpleHttp request = JsonSimpleHttp.doGet(userInfoUrl, session)
|
||||
.header("Authorization", "Bearer " + accessToken);
|
||||
JsonNode userInfo = JsonSimpleHttp.asJson(request);
|
||||
|
||||
name = getJsonProperty(userInfo, "name");
|
||||
preferredUsername = getJsonProperty(userInfo, "username");
|
||||
email = getJsonProperty(userInfo, "email");
|
||||
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
|
||||
}
|
||||
}
|
||||
identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
|
||||
identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
|
||||
processAccessTokenResponse(identity, tokenResponse);
|
||||
|
||||
identity.setId(id);
|
||||
identity.setName(name);
|
||||
identity.setEmail(email);
|
||||
|
||||
identity.setBrokerUserId(getConfig().getAlias() + "." + id);
|
||||
if (tokenResponse.getSessionState() != null) {
|
||||
identity.setBrokerSessionId(getConfig().getAlias() + "." + tokenResponse.getSessionState());
|
||||
}
|
||||
|
||||
if (preferredUsername == null) {
|
||||
preferredUsername = email;
|
||||
}
|
||||
|
||||
if (preferredUsername == null) {
|
||||
preferredUsername = id;
|
||||
}
|
||||
|
||||
identity.setUsername(preferredUsername);
|
||||
return identity;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.gitlab;
|
||||
|
||||
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
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 GitLabIdentityProviderFactory extends AbstractIdentityProviderFactory<GitLabIdentityProvider> implements SocialIdentityProviderFactory<GitLabIdentityProvider> {
|
||||
|
||||
public static final String PROVIDER_ID = "gitlab";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "GitLab";
|
||||
}
|
||||
|
||||
@Override
|
||||
public GitLabIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
|
||||
return new GitLabIdentityProvider(session, new OIDCIdentityProviderConfig(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -23,3 +23,4 @@ org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory
|
|||
org.keycloak.social.twitter.TwitterIdentityProviderFactory
|
||||
org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory
|
||||
org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory
|
||||
org.keycloak.social.gitlab.GitLabIdentityProviderFactory
|
||||
|
|
|
@ -574,6 +574,11 @@ key=Key
|
|||
stackoverflow.key.tooltip=The Key obtained from Stack Overflow client registration.
|
||||
openshift.base-url=Base Url
|
||||
openshift.base-url.tooltip=Base Url to Openshift Online API
|
||||
gitlab-application-id=Application Id
|
||||
gitlab-application-secret=Application Secret
|
||||
gitlab.application-id.tooltip=Application Id for the application you created in your GitLab Applications account menu
|
||||
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.
|
||||
|
||||
# User federation
|
||||
sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak
|
||||
|
|
|
@ -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> {{:: 'gitlab-application-id' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" id="clientId" type="text" ng-model="identityProvider.config.clientId" required>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'gitlab.application-id.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix">
|
||||
<label class="col-md-2 control-label" for="clientSecret"><span class="required">*</span> {{:: 'gitlab-application-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>{{:: 'gitlab.application-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>{{:: 'gitlab.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>
|
Loading…
Reference in a new issue