Support alternate OIDC providers, to prepare for the switch from Keycloak to `fabric8_auth` (#8650)
Allow switching to an alternate OIDC provider (provided that it emits access tokens as JWT tokens). This is the implementation required in upstream Che, for issues redhat-developer/rh-che#502 and redhat-developer/rh-che#525 Signed-off-by: David Festal <dfestal@redhat.com>6.19.x
parent
d8d24aece6
commit
ff3459d2d3
|
|
@ -99,9 +99,13 @@ che.organization.email.org_renamed_template=st-html-templates/organization_renam
|
|||
##### KEYCLOACK CONFIGURATION #####
|
||||
|
||||
# Url to keycloak identity provider server
|
||||
# Can be set to NULL only if `che.keycloak.oidcProvider`
|
||||
# is used
|
||||
che.keycloak.auth_server_url=http://${CHE_HOST}:5050/auth
|
||||
|
||||
# Keycloak realm is used to authenticate users
|
||||
# Can be set to NULL only if `che.keycloak.oidcProvider`
|
||||
# is used
|
||||
che.keycloak.realm=che
|
||||
|
||||
# Keycloak client id in che.keycloak.realm that is used by dashboard, ide and cli to authenticate users
|
||||
|
|
@ -117,3 +121,18 @@ che.keycloak.github.endpoint=NULL
|
|||
|
||||
# The number of seconds to tolerate for clock skew when verifying exp or nbf claims.
|
||||
che.keycloak.allowed_clock_skew_sec=3
|
||||
|
||||
# Use the OIDC optional `nonce` feature to increase security.
|
||||
che.keycloak.use_nonce=true
|
||||
|
||||
# URL to the Keycloak Javascript adapter we want to use.
|
||||
# if set to NULL, then the default used value is
|
||||
# `${che.keycloak.auth_server_url}/js/keycloak.js`,
|
||||
# or `<che-server>/api/keycloak/OIDCKeycloak.js`
|
||||
# if an alternate `oidc_provider` is used
|
||||
che.keycloak.js_adapter_url=NULL
|
||||
|
||||
# Base URL of an alternate OIDC provider that provides
|
||||
# a discovery endpoint as detailed in the following specification
|
||||
# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
|
||||
che.keycloak.oidc_provider=NULL
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -48,11 +48,19 @@ window.name = 'NG_DEFER_BOOTSTRAP!';
|
|||
|
||||
declare const Keycloak: Function;
|
||||
function buildKeycloakConfig(keycloakSettings: any) {
|
||||
return {
|
||||
url: keycloakSettings['che.keycloak.auth_server_url'],
|
||||
realm: keycloakSettings['che.keycloak.realm'],
|
||||
clientId: keycloakSettings['che.keycloak.client_id']
|
||||
};
|
||||
const theOidcProvider = keycloakSettings['che.keycloak.oidc_provider'];
|
||||
if (!theOidcProvider) {
|
||||
return {
|
||||
url: keycloakSettings['che.keycloak.auth_server_url'],
|
||||
realm: keycloakSettings['che.keycloak.realm'],
|
||||
clientId: keycloakSettings['che.keycloak.client_id']
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
oidcProvider: theOidcProvider,
|
||||
clientId: keycloakSettings['che.keycloak.client_id']
|
||||
};
|
||||
}
|
||||
}
|
||||
interface IResolveFn<T> {
|
||||
(value: T | PromiseLike<T>): Promise<T>;
|
||||
|
|
@ -64,18 +72,18 @@ function keycloakLoad(keycloakSettings: any) {
|
|||
return new Promise((resolve: IResolveFn<any>, reject: IRejectFn<any>) => {
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.src = keycloakSettings['che.keycloak.auth_server_url'] + '/js/keycloak.js';
|
||||
script.src = keycloakSettings['che.keycloak.js_adapter_url'];
|
||||
script.addEventListener('load', resolve);
|
||||
script.addEventListener('error', () => reject('Error loading script.'));
|
||||
script.addEventListener('abort', () => reject('Script loading aborted.'));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
function keycloakInit(keycloakConfig: any) {
|
||||
function keycloakInit(keycloakConfig: any, theUseNonce: boolean) {
|
||||
return new Promise((resolve: IResolveFn<any>, reject: IRejectFn<any>) => {
|
||||
const keycloak = Keycloak(keycloakConfig);
|
||||
keycloak.init({
|
||||
onLoad: 'login-required', checkLoginIframe: false
|
||||
onLoad: 'login-required', checkLoginIframe: false, useNonce: theUseNonce
|
||||
}).success(() => {
|
||||
resolve(keycloak);
|
||||
}).error((error: any) => {
|
||||
|
|
@ -101,7 +109,11 @@ angular.element(document).ready(() => {
|
|||
// load Keycloak
|
||||
return keycloakLoad(keycloakSettings).then(() => {
|
||||
// init Keycloak
|
||||
return keycloakInit(keycloakAuth.config);
|
||||
var useNonce: boolean;
|
||||
if (typeof keycloakSettings['che.keycloak.use_nonce'] === 'string') {
|
||||
useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true';
|
||||
}
|
||||
return keycloakInit(keycloakAuth.config, useNonce);
|
||||
}).then((keycloak: any) => {
|
||||
keycloakAuth.isPresent = true;
|
||||
keycloakAuth.keycloak = keycloak;
|
||||
|
|
|
|||
|
|
@ -47,4 +47,12 @@ export class ProfileController {
|
|||
editProfile(): void {
|
||||
this.$window.open(this.profileUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit profile - redirects to proper page.
|
||||
*/
|
||||
get cannotEdit: boolean {
|
||||
return !this.profileUrl;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
<che-label-container che-label-name="">
|
||||
<che-button-default class="prifile-add-button"
|
||||
che-button-title="Edit" name="editButton"
|
||||
ng-click="profileController.editProfile()"></che-button-default>
|
||||
ng-click="profileController.editProfile()"
|
||||
ng-disabled="profileController.cannotEdit"></che-button-default>
|
||||
</che-label-container>
|
||||
</md-content>
|
||||
|
|
|
|||
|
|
@ -563,12 +563,39 @@ CHE_INFRA_OPENSHIFT_TLS_ENABLED=false
|
|||
#
|
||||
#CHE_KEYCLOAK_OSO_ENDPOINT=NULL
|
||||
#CHE_KEYCLOAK_GITHUB_ENDPOINT=NULL
|
||||
#CHE_KEYCLOAK_AUTH__SERVER__URL=http://172.17.0.1:5050/auth
|
||||
#CHE_KEYCLOAK_REALM=che
|
||||
#CHE_KEYCLOAK_CLIENT__ID=che-public
|
||||
#CHE_KEYCLOAK_ALLOWED__CLOCK__SKEW__SEC=3
|
||||
#CHE_KEYCLOAK_ADMIN_REQUIRE_UPDATE_PASSWORD=true
|
||||
|
||||
#
|
||||
# Url to keycloak identity provider server
|
||||
# Can be set to NULL only if `CHE_KEYCLOAK_OIDC__PROVIDER`
|
||||
# is used
|
||||
#CHE_KEYCLOAK_AUTH__SERVER__URL=http://172.17.0.1:5050/auth
|
||||
|
||||
# Keycloak realm is used to authenticate users
|
||||
# Can be set to NULL only if `CHE_KEYCLOAK_OIDC__PROVIDER`
|
||||
# is used
|
||||
#CHE_KEYCLOAK_REALM=che
|
||||
|
||||
# Keycloak or OIDC client id used by dashboard,
|
||||
# ide and cli to authenticate users
|
||||
#CHE_KEYCLOAK_CLIENT__ID=che-public
|
||||
|
||||
# Use the OIDC optional `nonce` feature to increase security.
|
||||
#CHE_KEYCLOAK_USE__NONCE=true
|
||||
|
||||
# URL to the Keycloak Javascript adapter we want to use.
|
||||
# if set to NULL, then the default used value is
|
||||
# `${CHE_KEYCLOAK_AUTH__SERVER__URL}/js/keycloak.js`,
|
||||
# or `<che-server>/api/keycloak/OIDCKeycloak.js`
|
||||
# if an alternate `oidc_provider` is used
|
||||
#CHE_KEYCLOAK_JS__ADAPTER__URL=NULL
|
||||
|
||||
# Base URL of an alternate OIDC provider that provides
|
||||
# a discovery endpoint as detailed in the following specification
|
||||
# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
|
||||
#CHE_KEYCLOAK_OIDC__PROVIDER=NULL
|
||||
|
||||
|
||||
########################################################################################
|
||||
##### #####
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@
|
|||
script.type = "text/javascript";
|
||||
script.language = "javascript";
|
||||
script.async = true;
|
||||
script.src = keycloakSettings['che.keycloak.auth_server_url'] + '/js/keycloak.js';
|
||||
script.src = keycloakSettings['che.keycloak.js_adapter_url'];
|
||||
|
||||
script.onload = function() {
|
||||
Loader.initKeycloak(keycloakSettings);
|
||||
|
|
@ -114,16 +114,32 @@
|
|||
* Initialize keycloak and load the IDE
|
||||
*/
|
||||
this.initKeycloak = function(keycloakSettings) {
|
||||
var keycloak = Keycloak({
|
||||
url: keycloakSettings['che.keycloak.auth_server_url'],
|
||||
realm: keycloakSettings['che.keycloak.realm'],
|
||||
clientId: keycloakSettings['che.keycloak.client_id']
|
||||
});
|
||||
function keycloakConfig() {
|
||||
const theOidcProvider = keycloakSettings['che.keycloak.oidc_provider'];
|
||||
if (!theOidcProvider) {
|
||||
return {
|
||||
url: keycloakSettings['che.keycloak.auth_server_url'],
|
||||
realm: keycloakSettings['che.keycloak.realm'],
|
||||
clientId: keycloakSettings['che.keycloak.client_id']
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
oidcProvider: theOidcProvider,
|
||||
clientId: keycloakSettings['che.keycloak.client_id']
|
||||
};
|
||||
}
|
||||
}
|
||||
var keycloak = Keycloak(keycloakConfig());
|
||||
|
||||
window['_keycloak'] = keycloak;
|
||||
|
||||
var useNonce;
|
||||
if (typeof keycloakSettings['che.keycloak.use_nonce'] === 'string') {
|
||||
useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
keycloak
|
||||
.init({onLoad: 'login-required', checkLoginIframe: false})
|
||||
.init({onLoad: 'login-required', checkLoginIframe: false, useNonce: useNonce})
|
||||
.success(function(authenticated) {
|
||||
Loader.startLoading();
|
||||
})
|
||||
|
|
|
|||
|
|
@ -39,17 +39,30 @@ public final class Keycloak extends JavaScriptObject {
|
|||
}-*/;
|
||||
|
||||
public static native Promise<Keycloak> init(
|
||||
String theUrl, String theRealm, String theClientId) /*-{
|
||||
String theUrl,
|
||||
String theRealm,
|
||||
String theClientId,
|
||||
String theOidcProvider,
|
||||
boolean theUseNonce) /*-{
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
console.log('[Keycloak] Initializing');
|
||||
var keycloak = $wnd.Keycloak({
|
||||
url: theUrl,
|
||||
realm: theRealm,
|
||||
clientId: theClientId
|
||||
});
|
||||
var config;
|
||||
if(!theOidcProvider) {
|
||||
config = {
|
||||
url: theUrl,
|
||||
realm: theRealm,
|
||||
clientId: theClientId
|
||||
};
|
||||
} else {
|
||||
config = {
|
||||
oidcProvider: theOidcProvider,
|
||||
clientId: theClientId
|
||||
};
|
||||
}
|
||||
var keycloak = $wnd.Keycloak(config);
|
||||
$wnd['_keycloak'] = keycloak;
|
||||
keycloak.init({onLoad: 'login-required', checkLoginIframe: false})
|
||||
keycloak.init({onLoad: 'login-required', checkLoginIframe: false, useNonce: theUseNonce})
|
||||
.success(function (authenticated) {
|
||||
resolve(keycloak);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ package org.eclipse.che.multiuser.keycloak.ide;
|
|||
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JS_ADAPTER_URL_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OIDC_PROVIDER_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_NONCE_SETTING;
|
||||
|
||||
import com.google.gwt.core.client.Callback;
|
||||
import com.google.gwt.core.client.JavaScriptObject;
|
||||
|
|
@ -50,13 +53,15 @@ public class KeycloakProvider {
|
|||
return;
|
||||
}
|
||||
|
||||
String keycloakServerUrl = settings.get(AUTH_SERVER_URL_SETTING);
|
||||
String jsAdapterUrl = settings.get(JS_ADAPTER_URL_SETTING);
|
||||
|
||||
keycloak =
|
||||
CallbackPromiseHelper.createFromCallback(
|
||||
new CallbackPromiseHelper.Call<Void, Throwable>() {
|
||||
@Override
|
||||
public void makeCall(final Callback<Void, Throwable> callback) {
|
||||
ScriptInjector.fromUrl(
|
||||
settings.get(AUTH_SERVER_URL_SETTING) + "/js/keycloak.js")
|
||||
ScriptInjector.fromUrl(jsAdapterUrl)
|
||||
.setCallback(
|
||||
new Callback<Void, Exception>() {
|
||||
@Override
|
||||
|
|
@ -76,9 +81,11 @@ public class KeycloakProvider {
|
|||
.thenPromise(
|
||||
(v) ->
|
||||
Keycloak.init(
|
||||
settings.get(AUTH_SERVER_URL_SETTING),
|
||||
keycloakServerUrl,
|
||||
settings.get(REALM_SETTING),
|
||||
settings.get(CLIENT_ID_SETTING)));
|
||||
settings.get(CLIENT_ID_SETTING),
|
||||
settings.get(OIDC_PROVIDER_SETTING),
|
||||
Boolean.valueOf(settings.get(USE_NONCE_SETTING)).booleanValue()));
|
||||
Log.debug(getClass(), "Keycloak init complete: ", this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,18 @@
|
|||
<dto-generator-out-directory>${project.build.directory}/generated-sources/dto/</dto-generator-out-directory>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>jwks-rsa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ import javax.servlet.http.HttpServletRequest;
|
|||
public abstract class AbstractKeycloakFilter implements Filter {
|
||||
|
||||
protected boolean shouldSkipAuthentication(HttpServletRequest request, String token) {
|
||||
return token != null && token.startsWith("machine");
|
||||
return (token != null && token.startsWith("machine"))
|
||||
|| (request.getRequestURI() != null
|
||||
&& request.getRequestURI().endsWith("api/keycloak/OIDCKeycloak.js"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -10,25 +10,23 @@
|
|||
*/
|
||||
package org.eclipse.che.multiuser.keycloak.server;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.auth0.jwk.GuavaCachedJwkProvider;
|
||||
import com.auth0.jwk.Jwk;
|
||||
import com.auth0.jwk.JwkException;
|
||||
import com.auth0.jwk.JwkProvider;
|
||||
import com.auth0.jwk.UrlJwkProvider;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.JwsHeader;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureException;
|
||||
import java.io.BufferedReader;
|
||||
import io.jsonwebtoken.SigningKeyResolverAdapter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Key;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
|
@ -45,27 +43,25 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
@Singleton
|
||||
public class KeycloakAuthenticationFilter extends AbstractKeycloakFilter {
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final Type STRING_MAP_TYPE = new TypeToken<Map<String, String>>() {}.getType();
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(KeycloakAuthenticationFilter.class);
|
||||
|
||||
private String authServerUrl;
|
||||
private String realm;
|
||||
private String jwksUrl;
|
||||
private long allowedClockSkewSec;
|
||||
private PublicKey publicKey = null;
|
||||
private RequestTokenExtractor tokenExtractor;
|
||||
private JwkProvider jwkProvider;
|
||||
|
||||
@Inject
|
||||
public KeycloakAuthenticationFilter(
|
||||
@Named(KeycloakConstants.AUTH_SERVER_URL_SETTING) String authServerUrl,
|
||||
@Named(KeycloakConstants.REALM_SETTING) String realm,
|
||||
KeycloakSettings keycloakSettings,
|
||||
@Named(KeycloakConstants.ALLOWED_CLOCK_SKEW_SEC) long allowedClockSkewSec,
|
||||
RequestTokenExtractor tokenExtractor) {
|
||||
this.authServerUrl = authServerUrl;
|
||||
this.realm = realm;
|
||||
RequestTokenExtractor tokenExtractor)
|
||||
throws MalformedURLException {
|
||||
this.jwksUrl = keycloakSettings.get().get(KeycloakConstants.JWKS_ENDPOINT_SETTING);
|
||||
this.allowedClockSkewSec = allowedClockSkewSec;
|
||||
this.tokenExtractor = tokenExtractor;
|
||||
if (jwksUrl != null) {
|
||||
this.jwkProvider = new GuavaCachedJwkProvider(new UrlJwkProvider(new URL(jwksUrl)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -91,69 +87,55 @@ public class KeycloakAuthenticationFilter extends AbstractKeycloakFilter {
|
|||
jwt =
|
||||
Jwts.parser()
|
||||
.setAllowedClockSkewSeconds(allowedClockSkewSec)
|
||||
.setSigningKey(getJwtPublicKey(false))
|
||||
.setSigningKeyResolver(
|
||||
new SigningKeyResolverAdapter() {
|
||||
@Override
|
||||
public Key resolveSigningKey(
|
||||
@SuppressWarnings("rawtypes") JwsHeader header, Claims claims) {
|
||||
try {
|
||||
return getJwtPublicKey(header);
|
||||
} catch (JwkException e) {
|
||||
throw new JwtException(
|
||||
"Error during the retrieval of the public key during JWT token validation",
|
||||
e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.parseClaimsJws(token);
|
||||
LOG.debug("JWT = ", jwt);
|
||||
// OK, we can trust this JWT
|
||||
} catch (SignatureException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidKeySpecException
|
||||
| IllegalArgumentException e) {
|
||||
} catch (SignatureException | IllegalArgumentException e) {
|
||||
// don't trust the JWT!
|
||||
LOG.error("Failed verifying the JWT token", e);
|
||||
try {
|
||||
LOG.info("Retrying after updating the public key", e);
|
||||
jwt =
|
||||
Jwts.parser()
|
||||
.setAllowedClockSkewSeconds(allowedClockSkewSec)
|
||||
.setSigningKey(getJwtPublicKey(true))
|
||||
.parseClaimsJws(token);
|
||||
LOG.debug("JWT = ", jwt);
|
||||
// OK, we can trust this JWT
|
||||
} catch (SignatureException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidKeySpecException
|
||||
| IllegalArgumentException ee) {
|
||||
// don't trust the JWT!
|
||||
LOG.error("Failed verifying the JWT token after public key update", e);
|
||||
send403(res);
|
||||
return;
|
||||
}
|
||||
send403(res);
|
||||
return;
|
||||
}
|
||||
request.setAttribute("token", jwt);
|
||||
chain.doFilter(req, res);
|
||||
}
|
||||
|
||||
private synchronized PublicKey getJwtPublicKey(boolean reset)
|
||||
throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
if (reset) {
|
||||
publicKey = null;
|
||||
private synchronized PublicKey getJwtPublicKey(JwsHeader<?> header) throws JwkException {
|
||||
String kid = header.getKeyId();
|
||||
if (kid == null) {
|
||||
LOG.warn(
|
||||
"'kid' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys");
|
||||
return null;
|
||||
}
|
||||
if (publicKey == null) {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(authServerUrl + "/realms/" + realm);
|
||||
LOG.info("Pulling realm public key from URL : {}", url);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
Map<String, String> realmSettings;
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
|
||||
realmSettings = GSON.fromJson(in, STRING_MAP_TYPE);
|
||||
}
|
||||
String encodedPublicKey = realmSettings.get("public_key");
|
||||
byte[] decoded = Base64.getDecoder().decode(encodedPublicKey);
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded);
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||
publicKey = kf.generatePublic(keySpec);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Exception during retrieval of the Keycloak realm public key", e);
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
String alg = header.getAlgorithm();
|
||||
if (alg == null) {
|
||||
LOG.warn(
|
||||
"'alg' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys");
|
||||
return null;
|
||||
}
|
||||
return publicKey;
|
||||
|
||||
if (jwkProvider == null) {
|
||||
LOG.warn(
|
||||
"JWK provider is not available: This is not possible to validate the token with OIDC provider keys.\n"
|
||||
+ "Please look into the startup logs to find out the root cause");
|
||||
return null;
|
||||
}
|
||||
Jwk jwk = jwkProvider.get(kid);
|
||||
return jwk.getPublicKey();
|
||||
}
|
||||
|
||||
private void send403(ServletResponse res) throws IOException {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ package org.eclipse.che.multiuser.keycloak.server;
|
|||
|
||||
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
|
@ -43,4 +48,25 @@ public class KeycloakConfigurationService extends Service {
|
|||
public Map<String, String> settings() {
|
||||
return keycloakSettings.get();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/OIDCKeycloak.js")
|
||||
@Produces("text/javascript")
|
||||
public String javascriptAdapter() throws IOException {
|
||||
URL resource =
|
||||
Thread.currentThread().getContextClassLoader().getResource("keycloak/OIDCKeycloak.js");
|
||||
if (resource != null) {
|
||||
URLConnection conn = resource.openConnection();
|
||||
try (InputStream is = conn.getInputStream();
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, length);
|
||||
}
|
||||
return os.toString("UTF-8");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,48 +13,134 @@ package org.eclipse.che.multiuser.keycloak.server;
|
|||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.GITHUB_ENDPOINT_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JS_ADAPTER_URL_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JWKS_ENDPOINT_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.LOGOUT_ENDPOINT_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OIDC_PROVIDER_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OSO_ENDPOINT_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.PASSWORD_ENDPOINT_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.PROFILE_ENDPOINT_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.TOKEN_ENDPOINT_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USERINFO_ENDPOINT_SETTING;
|
||||
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_NONCE_SETTING;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.Maps;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import org.eclipse.che.commons.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** @author Max Shaposhnik (mshaposh@redhat.com) */
|
||||
@Singleton
|
||||
public class KeycloakSettings {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(KeycloakSettings.class);
|
||||
|
||||
private final Map<String, String> settings;
|
||||
|
||||
@Inject
|
||||
public KeycloakSettings(
|
||||
@Named(AUTH_SERVER_URL_SETTING) String serverURL,
|
||||
@Named(REALM_SETTING) String realm,
|
||||
@Nullable @Named(JS_ADAPTER_URL_SETTING) String jsAdapterUrl,
|
||||
@Nullable @Named(AUTH_SERVER_URL_SETTING) String serverURL,
|
||||
@Nullable @Named(REALM_SETTING) String realm,
|
||||
@Named(CLIENT_ID_SETTING) String clientId,
|
||||
@Nullable @Named(OIDC_PROVIDER_SETTING) String oidcProvider,
|
||||
@Named(USE_NONCE_SETTING) boolean useNonce,
|
||||
@Nullable @Named(OSO_ENDPOINT_SETTING) String osoEndpoint,
|
||||
@Nullable @Named(GITHUB_ENDPOINT_SETTING) String gitHubEndpoint) {
|
||||
|
||||
if (serverURL == null && oidcProvider == null) {
|
||||
throw new RuntimeException(
|
||||
"Either the '"
|
||||
+ AUTH_SERVER_URL_SETTING
|
||||
+ "' or '"
|
||||
+ OIDC_PROVIDER_SETTING
|
||||
+ "' property should be set");
|
||||
}
|
||||
|
||||
if (oidcProvider == null && realm == null) {
|
||||
throw new RuntimeException("The '" + REALM_SETTING + "' property should be set");
|
||||
}
|
||||
|
||||
String wellKnownEndpoint = oidcProvider != null ? oidcProvider : serverURL + "/realms/" + realm;
|
||||
if (!wellKnownEndpoint.endsWith("/")) {
|
||||
wellKnownEndpoint = wellKnownEndpoint + "/";
|
||||
}
|
||||
wellKnownEndpoint += ".well-known/openid-configuration";
|
||||
|
||||
LOG.info("Retrieving OpenId configuration from endpoint: {}", wellKnownEndpoint);
|
||||
|
||||
URL url;
|
||||
Map<String, Object> openIdConfiguration;
|
||||
try {
|
||||
url = new URL(wellKnownEndpoint);
|
||||
final InputStream inputStream = url.openStream();
|
||||
final JsonFactory factory = new JsonFactory();
|
||||
final JsonParser parser = factory.createParser(inputStream);
|
||||
final TypeReference<Map<String, Object>> typeReference =
|
||||
new TypeReference<Map<String, Object>>() {};
|
||||
openIdConfiguration = new ObjectMapper().reader().readValue(parser, typeReference);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(
|
||||
"Exception while retrieving OpenId configuration from endpoint: " + wellKnownEndpoint, e);
|
||||
}
|
||||
|
||||
LOG.info("openid configuration = {}", openIdConfiguration);
|
||||
|
||||
Map<String, String> settings = Maps.newHashMap();
|
||||
settings.put(AUTH_SERVER_URL_SETTING, serverURL);
|
||||
settings.put(CLIENT_ID_SETTING, clientId);
|
||||
settings.put(REALM_SETTING, realm);
|
||||
settings.put(PROFILE_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/account");
|
||||
settings.put(PASSWORD_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/account/password");
|
||||
settings.put(
|
||||
LOGOUT_ENDPOINT_SETTING,
|
||||
serverURL + "/realms/" + realm + "/protocol/openid-connect/logout");
|
||||
settings.put(
|
||||
TOKEN_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/protocol/openid-connect/token");
|
||||
if (serverURL != null) {
|
||||
settings.put(AUTH_SERVER_URL_SETTING, serverURL);
|
||||
settings.put(PROFILE_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/account");
|
||||
settings.put(PASSWORD_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/account/password");
|
||||
settings.put(
|
||||
LOGOUT_ENDPOINT_SETTING,
|
||||
serverURL + "/realms/" + realm + "/protocol/openid-connect/logout");
|
||||
settings.put(
|
||||
TOKEN_ENDPOINT_SETTING,
|
||||
serverURL + "/realms/" + realm + "/protocol/openid-connect/token");
|
||||
}
|
||||
String endSessionEndpoint = (String) openIdConfiguration.get("end_session_endpoint");
|
||||
if (endSessionEndpoint != null) {
|
||||
settings.put(LOGOUT_ENDPOINT_SETTING, endSessionEndpoint);
|
||||
}
|
||||
String tokenEndpoint = (String) openIdConfiguration.get("token_endpoint");
|
||||
if (tokenEndpoint != null) {
|
||||
settings.put(TOKEN_ENDPOINT_SETTING, tokenEndpoint);
|
||||
}
|
||||
String userInfoEndpoint = (String) openIdConfiguration.get("userinfo_endpoint");
|
||||
if (userInfoEndpoint != null) {
|
||||
settings.put(USERINFO_ENDPOINT_SETTING, userInfoEndpoint);
|
||||
}
|
||||
String jwksUriEndpoint = (String) openIdConfiguration.get("jwks_uri");
|
||||
if (jwksUriEndpoint != null) {
|
||||
settings.put(JWKS_ENDPOINT_SETTING, jwksUriEndpoint);
|
||||
}
|
||||
settings.put(OSO_ENDPOINT_SETTING, osoEndpoint);
|
||||
settings.put(GITHUB_ENDPOINT_SETTING, gitHubEndpoint);
|
||||
|
||||
if (oidcProvider != null) {
|
||||
settings.put(OIDC_PROVIDER_SETTING, oidcProvider);
|
||||
}
|
||||
settings.put(USE_NONCE_SETTING, Boolean.toString(useNonce));
|
||||
if (jsAdapterUrl == null) {
|
||||
jsAdapterUrl =
|
||||
(oidcProvider != null) ? "/api/keycloak/OIDCKeycloak.js" : serverURL + "/js/keycloak.js";
|
||||
}
|
||||
settings.put(JS_ADAPTER_URL_SETTING, jsAdapterUrl);
|
||||
|
||||
this.settings = Collections.unmodifiableMap(settings);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import java.io.IOException;
|
|||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.eclipse.che.api.core.ApiException;
|
||||
import org.eclipse.che.api.core.ConflictException;
|
||||
import org.eclipse.che.api.core.NotFoundException;
|
||||
|
|
@ -25,6 +24,7 @@ import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
|
|||
import org.eclipse.che.api.user.server.model.impl.ProfileImpl;
|
||||
import org.eclipse.che.api.user.server.spi.ProfileDao;
|
||||
import org.eclipse.che.commons.env.EnvironmentContext;
|
||||
import org.eclipse.che.multiuser.keycloak.server.KeycloakSettings;
|
||||
import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
@ -43,12 +43,10 @@ public class KeycloakProfileDao implements ProfileDao {
|
|||
|
||||
@Inject
|
||||
public KeycloakProfileDao(
|
||||
@Named(KeycloakConstants.AUTH_SERVER_URL_SETTING) String authServerUrl,
|
||||
@Named(KeycloakConstants.REALM_SETTING) String realm,
|
||||
HttpJsonRequestFactory requestFactory) {
|
||||
KeycloakSettings keycloakSettings, HttpJsonRequestFactory requestFactory) {
|
||||
this.requestFactory = requestFactory;
|
||||
this.keyclockCurrentUserInfoUrl =
|
||||
authServerUrl + "/realms/" + realm + "/protocol/openid-connect/userinfo";
|
||||
keycloakSettings.get().get(KeycloakConstants.USERINFO_ENDPOINT_SETTING);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ public class KeycloakConstants {
|
|||
public static final String AUTH_SERVER_URL_SETTING = KEYCLOAK_SETTING_PREFIX + "auth_server_url";
|
||||
public static final String REALM_SETTING = KEYCLOAK_SETTING_PREFIX + "realm";
|
||||
public static final String CLIENT_ID_SETTING = KEYCLOAK_SETTING_PREFIX + "client_id";
|
||||
public static final String OIDC_PROVIDER_SETTING = KEYCLOAK_SETTING_PREFIX + "oidc_provider";
|
||||
public static final String USE_NONCE_SETTING = KEYCLOAK_SETTING_PREFIX + "use_nonce";
|
||||
public static final String JS_ADAPTER_URL_SETTING = KEYCLOAK_SETTING_PREFIX + "js_adapter_url";
|
||||
public static final String ALLOWED_CLOCK_SKEW_SEC =
|
||||
KEYCLOAK_SETTING_PREFIX + "allowed_clock_skew_sec";
|
||||
|
||||
|
|
@ -29,6 +32,9 @@ public class KeycloakConstants {
|
|||
KEYCLOAK_SETTING_PREFIX + "password.endpoint";
|
||||
public static final String LOGOUT_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "logout.endpoint";
|
||||
public static final String TOKEN_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "token.endpoint";
|
||||
public static final String JWKS_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "jwks.endpoint";
|
||||
public static final String USERINFO_ENDPOINT_SETTING =
|
||||
KEYCLOAK_SETTING_PREFIX + "userinfo.endpoint";
|
||||
public static final String GITHUB_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "github.endpoint";
|
||||
|
||||
public static String getEndpoint(String apiEndpoint) {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export class KeycloakLoader {
|
|||
script.type = 'text/javascript';
|
||||
(script as any).language = 'javascript';
|
||||
script.async = true;
|
||||
script.src = keycloakSettings['che.keycloak.auth_server_url'] + '/js/keycloak.js';
|
||||
script.src = keycloakSettings['che.keycloak.js_adapter_url'];
|
||||
|
||||
script.onload = () => {
|
||||
resolve(this.initKeycloak(keycloakSettings));
|
||||
|
|
@ -79,16 +79,31 @@ export class KeycloakLoader {
|
|||
*/
|
||||
private initKeycloak(keycloakSettings: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const keycloak = Keycloak({
|
||||
url: keycloakSettings['che.keycloak.auth_server_url'],
|
||||
realm: keycloakSettings['che.keycloak.realm'],
|
||||
clientId: keycloakSettings['che.keycloak.client_id']
|
||||
});
|
||||
function keycloakConfig() {
|
||||
const theOidcProvider = keycloakSettings['che.keycloak.oidc_provider'];
|
||||
if (!theOidcProvider) {
|
||||
return {
|
||||
url: keycloakSettings['che.keycloak.auth_server_url'],
|
||||
realm: keycloakSettings['che.keycloak.realm'],
|
||||
clientId: keycloakSettings['che.keycloak.client_id']
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
oidcProvider: theOidcProvider,
|
||||
clientId: keycloakSettings['che.keycloak.client_id']
|
||||
};
|
||||
}
|
||||
}
|
||||
const keycloak = Keycloak(keycloakConfig());
|
||||
|
||||
window['_keycloak'] = keycloak;
|
||||
|
||||
var useNonce;
|
||||
if (typeof keycloakSettings['che.keycloak.use_nonce'] === 'string') {
|
||||
useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true';
|
||||
}
|
||||
keycloak
|
||||
.init({onLoad: 'login-required', checkLoginIframe: false})
|
||||
.init({onLoad: 'login-required', checkLoginIframe: false, useNonce: useNonce})
|
||||
.success(() => {
|
||||
resolve(keycloak);
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue