Compare commits

...
61 changed files with 5500 additions and 60 deletions

No files matched your search

+32 -19
View File
@@ -31,25 +31,38 @@
<packaging>pom</packaging>
<modules>
<module>adapter-core</module>
<module>as7-eap6</module>
<module>installed</module>
<module>fuse7</module>
<module>kcinit</module>
<module>jaxrs-oauth-client</module>
<module>jetty</module>
<module>js</module>
<module>osgi-adapter</module>
<module>servlet-filter</module>
<module>servlet-oauth-client</module>
<module>spring-boot</module>
<module>spring-boot2</module>
<module>spring-boot-adapter-core</module>
<module>spring-boot-container-bundle</module>
<module>spring-security</module>
<module>tomcat</module>
<module>undertow</module>
<module>wildfly</module>
<module>wildfly-elytron</module>
</modules>
<profiles>
<profile>
<id>nonServer</id>
<activation>
<property>
<name>!serverOnly</name>
</property>
</activation>
<modules>
<module>adapter-core</module>
<module>as7-eap6</module>
<module>installed</module>
<module>fuse7</module>
<module>kcinit</module>
<module>jaxrs-oauth-client</module>
<module>jetty</module>
<module>osgi-adapter</module>
<module>servlet-filter</module>
<module>servlet-oauth-client</module>
<module>spring-boot</module>
<module>spring-boot2</module>
<module>spring-boot-adapter-core</module>
<module>spring-boot-container-bundle</module>
<module>spring-security</module>
<module>tomcat</module>
<module>undertow</module>
<module>wildfly</module>
<module>wildfly-elytron</module>
</modules>
</profile>
</profiles>
</project>
+15 -2
View File
@@ -31,8 +31,21 @@
<packaging>pom</packaging>
<modules>
<module>spi</module>
<module>saml</module>
<module>oidc</module>
</modules>
<profiles>
<profile>
<id>nonServer</id>
<activation>
<property>
<name>!serverOnly</name>
</property>
</activation>
<modules>
<module>spi</module>
<module>saml</module>
</modules>
</profile>
</profiles>
</project>
+14 -1
View File
@@ -19,6 +19,19 @@
<modules>
<module>policy</module>
<module>client</module>
</modules>
<profiles>
<profile>
<id>nonServer</id>
<activation>
<property>
<name>!serverOnly</name>
</property>
</activation>
<modules>
<module>client</module>
</modules>
</profile>
</profiles>
</project>
+13 -1
View File
@@ -44,7 +44,6 @@
</licenses>
<modules>
<module>adapter</module>
<module>spi</module>
</modules>
@@ -77,5 +76,18 @@
</plugins>
</build>
</profile>
<profile>
<id>nonServer</id>
<activation>
<property>
<name>!serverOnly</name>
</property>
</activation>
<modules>
<module>adapter</module>
</modules>
</profile>
</profiles>
</project>
+14 -1
View File
@@ -31,7 +31,20 @@
<packaging>pom</packaging>
<modules>
<module>adapter-feature-pack</module>
<module>server-feature-pack</module>
</modules>
<profiles>
<profile>
<id>nonServer</id>
<activation>
<property>
<name>!serverOnly</name>
</property>
</activation>
<modules>
<module>adapter-feature-pack</module>
</modules>
</profile>
</profiles>
</project>
+14 -3
View File
@@ -36,16 +36,27 @@
</properties>
<modules>
<module>adapters</module>
<module>saml-adapters</module>
<module>feature-packs</module>
<module>licenses-common</module>
<module>maven-plugins</module>
<module>server-dist</module>
<module>server-overlay</module>
</modules>
<profiles>
<profile>
<id>nonServer</id>
<activation>
<property>
<name>!serverOnly</name>
</property>
</activation>
<modules>
<module>adapters</module>
<module>saml-adapters</module>
<module>server-overlay</module>
</modules>
</profile>
<profile>
<id>jboss-release</id>
<modules>
+15 -2
View File
@@ -31,8 +31,21 @@
<packaging>pom</packaging>
<modules>
<module>admin-client</module>
<module>client-registration</module>
<module>client-cli</module>
</modules>
<profiles>
<profile>
<id>nonServer</id>
<activation>
<property>
<name>!serverOnly</name>
</property>
</activation>
<modules>
<module>admin-client</module>
<module>client-registration</module>
</modules>
</profile>
</profiles>
</project>
+15 -7
View File
@@ -197,13 +197,10 @@
<module>services</module>
<module>themes</module>
<module>model</module>
<module>util</module>
<module>wildfly</module>
<module>integration</module>
<module>adapters</module>
<module>authz</module>
<module>examples</module>
<module>misc</module>
</modules>
<dependencyManagement>
@@ -1605,9 +1602,6 @@
<product.version>${project.version}</product.version>
<product.default-profile>community</product.default-profile>
</properties>
<modules>
<module>proxy</module>
</modules>
</profile>
<profile>
@@ -1633,7 +1627,7 @@
<id>testsuite</id>
<activation>
<property>
<name>!skipTestsuite</name>
<name>!skipTests</name>
</property>
</activation>
<modules>
@@ -1641,6 +1635,20 @@
</modules>
</profile>
<profile>
<id>nonServer</id>
<activation>
<property>
<name>!serverOnly</name>
</property>
</activation>
<modules>
<module>examples</module>
<module>misc</module>
<module>util</module>
</modules>
</profile>
<profile>
<id>distribution</id>
<modules>
@@ -91,5 +91,5 @@ public interface Errors {
String UNKNOWN_IDENTITY_PROVIDER = "unknown_identity_provider";
String ILLEGAL_ORIGIN = "illegal_origin";
String DISPLAY_UNSUPPORTED = "display_unsupported";
String INVALID_SCOPE = "invalid_scope";
}
@@ -50,6 +50,8 @@ public class DefaultAuthenticationFlows {
public static final String IDP_REVIEW_PROFILE_CONFIG_ALIAS = "review profile config";
public static final String IDP_CREATE_UNIQUE_USER_CONFIG_ALIAS = "create unique user config";
public static final String OPENSHIFT_CHALLENGE_FLOW = "openshift challenge flow";
public static final String OPENSHIFT_CHALLENGE_FLOW_DESCRIPTION = "openshift flow for clients requiring challenge mode";
public static void addFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm);
@@ -60,6 +62,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
if (realm.getFlowByAlias(OPENSHIFT_CHALLENGE_FLOW) == null) openshiftChallengeFlow(realm);
}
public static void migrateFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
@@ -70,6 +73,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
if (realm.getFlowByAlias(OPENSHIFT_CHALLENGE_FLOW) == null) openshiftChallengeFlow(realm);
}
public static void registrationFlow(RealmModel realm) {
@@ -562,4 +566,47 @@ public class DefaultAuthenticationFlows {
realm.addAuthenticatorExecution(execution);
}
public static void openshiftChallengeFlow(RealmModel realm) {
AuthenticationFlowModel challengeFlow = new AuthenticationFlowModel();
challengeFlow.setAlias(OPENSHIFT_CHALLENGE_FLOW);
challengeFlow.setDescription(OPENSHIFT_CHALLENGE_FLOW_DESCRIPTION);
challengeFlow.setProviderId("basic-flow");
challengeFlow.setTopLevel(true);
challengeFlow.setBuiltIn(true);
challengeFlow = realm.addAuthenticationFlow(challengeFlow);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("no-cookie-redirect");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("basic-auth");
execution.setPriority(20);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("basic-auth-otp");
execution.setPriority(30);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("auth-spnego");
execution.setPriority(40);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
}
}
@@ -135,5 +135,20 @@ public abstract class AbstractClientStorageAdapter extends UnsupportedOperations
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof ClientModel)) return false;
ClientModel that = (ClientModel) o;
return that.getId().equals(getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
}
@@ -263,4 +263,16 @@ public abstract class AbstractReadOnlyClientStorageAdapter extends AbstractClien
throw new ReadOnlyException("client is read only for this update");
}
@Override
public void setDisplayOnConsentScreen(boolean displayOnConsentScreen) {
throw new ReadOnlyException("client is read only for this update");
}
@Override
public void setConsentScreenText(String consentScreenText) {
throw new ReadOnlyException("client is read only for this update");
}
}
@@ -0,0 +1,117 @@
/*
* 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.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.util.BasicAuthHelper;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BasicAuthAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
@Override
public boolean requiresUser() {
return false;
}
@Override
public void authenticate(AuthenticationFlowContext context) {
String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null) {
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
String username = usernameSecret[0];
String password = usernameSecret[1];
MultivaluedMap<String, String> map = new MultivaluedHashMap<>();
map.putSingle(AuthenticationManager.FORM_USERNAME, username);
map.putSingle(CredentialRepresentation.PASSWORD, password);
if (!validateUserAndPassword(context, map)) {
return;
}
context.success();
} else {
Response response = challengeResponse(context);
context.challenge(response);
}
}
protected Response challengeResponse(AuthenticationFlowContext context) {
return Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, getHeader(context)).build();
}
private String getHeader(AuthenticationFlowContext context) {
return "Basic realm=\"" + context.getRealm().getName() + "\"";
}
@Override
protected Response invalidUser(AuthenticationFlowContext context) {
return challengeResponse(context);
}
@Override
protected Response disabledUser(AuthenticationFlowContext context) {
return challengeResponse(context);
}
@Override
protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
return challengeResponse(context);
}
@Override
protected Response invalidCredentials(AuthenticationFlowContext context) {
return challengeResponse(context);
}
@Override
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
return challengeResponse(context);
}
@Override
public void action(AuthenticationFlowContext context) {
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
}
@@ -0,0 +1,104 @@
/*
* 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.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.Collections;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BasicAuthAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "basic-auth";
public static final BasicAuthAuthenticator SINGLETON = new BasicAuthAuthenticator();
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return UserCredentialModel.PASSWORD;
}
@Override
public boolean isConfigurable() {
return false;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getDisplayType() {
return "Basic Auth Challenge";
}
@Override
public String getHelpText() {
return "Basic auth challenge.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.EMPTY_LIST;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}
@@ -0,0 +1,132 @@
/*
* 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.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.*;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.util.BasicAuthHelper;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BasicAuthOTPAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
@Override
public boolean requiresUser() {
return false;
}
@Override
public void authenticate(AuthenticationFlowContext context) {
String authorizationHeader = context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null) {
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader);
String username = usernameSecret[0];
String password = usernameSecret[1];
OTPPolicy otpPolicy = context.getRealm().getOTPPolicy();
int otpLength = otpPolicy.getDigits();
if (password.length() < otpLength) {
Response response = challengeResponse(context);
context.challenge(response);
return;
}
String otp = password.substring(password.length() - otpLength);
password = password.substring(0, password.length() - otpLength);
MultivaluedMap<String, String> map = new MultivaluedHashMap<>();
map.putSingle(AuthenticationManager.FORM_USERNAME, username);
map.putSingle(CredentialRepresentation.PASSWORD, password);
if (!validateUserAndPassword(context, map)) {
return;
}
boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(),
UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), otp));
if (!valid) {
context.setUser(null);
Response response = challengeResponse(context);
context.challenge(response);
} else {
context.success();
}
} else {
Response response = challengeResponse(context);
context.challenge(response);
}
}
protected Response challengeResponse(AuthenticationFlowContext context) {
return Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, getHeader(context)).build();
}
private String getHeader(AuthenticationFlowContext context) {
return "Basic realm=\"" + context.getRealm().getName() + "\"";
}
@Override
protected Response invalidUser(AuthenticationFlowContext context) {
return challengeResponse(context);
}
@Override
protected Response disabledUser(AuthenticationFlowContext context) {
return challengeResponse(context);
}
@Override
protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
return challengeResponse(context);
}
@Override
protected Response invalidCredentials(AuthenticationFlowContext context) {
return challengeResponse(context);
}
@Override
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
return challengeResponse(context);
}
@Override
public void action(AuthenticationFlowContext context) {
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return session.userCredentialManager().isConfiguredFor(realm, user, realm.getOTPPolicy().getType());
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
}
@@ -0,0 +1,104 @@
/*
* 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.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.Collections;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BasicAuthOTPAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "basic-auth-otp";
public static final BasicAuthOTPAuthenticator SINGLETON = new BasicAuthOTPAuthenticator();
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return UserCredentialModel.PASSWORD;
}
@Override
public boolean isConfigurable() {
return false;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getDisplayType() {
return "Basic Auth Password+OTP";
}
@Override
public String getHelpText() {
return "Basic auth challenge with OTP. Password param should contain a combination of password + otp. Realm's OTP policy is used to determine how to parse this. This SHOULD NOT BE USED in conjection with regular basic auth provider.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.EMPTY_LIST;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}
@@ -0,0 +1,70 @@
/*
* 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.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.resources.LoginActionsService;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class NoCookieFlowRedirectAuthenticator implements Authenticator {
@Override
public boolean requiresUser() {
return false;
}
@Override
public void authenticate(AuthenticationFlowContext context) {
if (!context.getHttpRequest().getUri().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
Response response = Response.status(302).header(HttpHeaders.LOCATION, context.getRefreshUrl(true)).build();
context.challenge(response);
return;
}
context.success();
}
@Override
public void action(AuthenticationFlowContext context) {
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
}
@@ -0,0 +1,104 @@
/*
* 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.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.Collections;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class NoCookieFlowRedirectAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "no-cookie-redirect";
public static final NoCookieFlowRedirectAuthenticator SINGLETON = new NoCookieFlowRedirectAuthenticator();
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return UserCredentialModel.PASSWORD;
}
@Override
public boolean isConfigurable() {
return false;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getDisplayType() {
return "Browser Redirect/Refresh";
}
@Override
public String getHelpText() {
return "Perform a 302 redirect to get user agent's current URI on authenticate path with an auth_session_id query parameter. This is for client's that do not support cookies.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.EMPTY_LIST;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}
@@ -0,0 +1,40 @@
/*
* 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.protocol.kubernetes;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Kubernetes extends OIDCLoginProtocol {
public Kubernetes(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, EventBuilder event) {
super(session, realm, uriInfo, headers, event);
}
public Kubernetes() {
}
}
@@ -0,0 +1,116 @@
/*
* 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.protocol.kubernetes;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.util.UriUtils;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientTemplateRepresentation;
import org.keycloak.services.ServicesLogger;
import java.util.*;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KubernetesLoginProtocolFactory extends OIDCLoginProtocolFactory {
private static final Logger logger = Logger.getLogger(KubernetesLoginProtocolFactory.class);
public static final String LOGIN_PROTOCOL = "kubernetes";
@Override
public LoginProtocol create(KeycloakSession session) {
return new Kubernetes().setSession(session);
}
@Override
protected void addDefaults(ClientModel client) {
// OIDC will add defaults
}
@Override
public Object createProtocolEndpoint(RealmModel realm, EventBuilder event) {
return new KubernetesProtocolEndpoint(realm, event);
}
@Override
public String getId() {
return LOGIN_PROTOCOL;
}
@Override
public void setupClientDefaults(ClientRepresentation rep, ClientModel newClient) {
if (rep.getRootUrl() != null && (rep.getRedirectUris() == null || rep.getRedirectUris().isEmpty())) {
String root = rep.getRootUrl();
if (root.endsWith("/")) root = root + "*";
else root = root + "/*";
newClient.addRedirectUri(root);
Set<String> origins = new HashSet<String>();
String origin = UriUtils.getOrigin(root);
logger.debugv("adding default client origin: {0}" , origin);
origins.add(origin);
newClient.setWebOrigins(origins);
}
if (rep.isBearerOnly() == null
&& rep.isPublicClient() == null) {
newClient.setPublicClient(true);
}
if (rep.isBearerOnly() == null) newClient.setBearerOnly(false);
// Backwards compatibility only
if (rep.isDirectGrantsOnly() != null) {
ServicesLogger.LOGGER.usingDeprecatedDirectGrantsOnly();
newClient.setStandardFlowEnabled(!rep.isDirectGrantsOnly());
newClient.setDirectAccessGrantsEnabled(rep.isDirectGrantsOnly());
} else {
if (rep.isStandardFlowEnabled() == null) newClient.setStandardFlowEnabled(true);
if (rep.isDirectAccessGrantsEnabled() == null) newClient.setDirectAccessGrantsEnabled(true);
}
if (rep.isImplicitFlowEnabled() == null) newClient.setImplicitFlowEnabled(true);
if (rep.isPublicClient() == null) newClient.setPublicClient(true);
if (rep.isFrontchannelLogout() == null) newClient.setFrontchannelLogout(false);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public Map<String, ProtocolMapperModel> getBuiltinMappers() {
return Collections.EMPTY_MAP;
}
@Override
protected void createDefaultClientScopesImpl(RealmModel newRealm) {
}
}
@@ -0,0 +1,87 @@
/*
* 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.protocol.kubernetes;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.services.resources.RealmsResource;
import javax.ws.rs.Path;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KubernetesProtocolEndpoint extends OIDCLoginProtocolService {
public KubernetesProtocolEndpoint(RealmModel realm, EventBuilder event) {
super(realm, event);
}
/**
* Token endpoint
*/
@Path("token")
public Object token() {
KubernetesTokenEndpoint endpoint = new KubernetesTokenEndpoint(tokenManager, realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
return endpoint;
}
public static UriBuilder tokenReviewnUrl(UriBuilder baseUriBuilder) {
return tokenUrl(baseUriBuilder).path(KubernetesProtocolEndpoint.class, "tokenReview");
}
public static UriBuilder tokenServiceBaseUrl(UriBuilder baseUriBuilder) {
return baseUriBuilder.path(RealmsResource.class).path("{realm}/protocol/" + KubernetesLoginProtocolFactory.LOGIN_PROTOCOL);
}
public static UriBuilder authUrl(UriInfo uriInfo) {
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return authUrl(baseUriBuilder);
}
public static UriBuilder authUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "auth");
}
public static UriBuilder tokenUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "token");
}
public static UriBuilder userInfoUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "issueUserInfo");
}
public static UriBuilder logoutUrl(UriInfo uriInfo) {
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return logoutUrl(baseUriBuilder);
}
public static UriBuilder logoutUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "logout");
}
}
@@ -0,0 +1,144 @@
/*
* 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.protocol.kubernetes;
import org.keycloak.RSATokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.Urls;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.PublicKey;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KubernetesTokenEndpoint extends TokenEndpoint {
public KubernetesTokenEndpoint(TokenManager tokenManager, RealmModel realm, EventBuilder event) {
super(tokenManager, realm, event);
}
@Path("token-review")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response tokenReview(TokenReviewRequestRepresentation reviewRequest) throws Exception {
event.event(EventType.INTROSPECT_TOKEN);
checkSsl();
checkRealm();
AccessToken toIntrospect = null;
String token = reviewRequest.getSpec().getToken();
try {
RSATokenVerifier verifier = RSATokenVerifier.create(token)
.realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
if (publicKey == null) {
event.detail(Details.REASON, "invalid public key");
event.error(Errors.INVALID_TOKEN);
return Response.status(401).entity(TokenReviewResponseRepresentation.error("invalid token")).type(MediaType.APPLICATION_JSON_TYPE).build();
} else {
verifier.publicKey(publicKey);
verifier.verify();
toIntrospect = verifier.getToken();
}
} catch (VerificationException e) {
event.detail(Details.REASON, "token verification failure");
event.error(Errors.INVALID_TOKEN);
return Response.status(401).entity(TokenReviewResponseRepresentation.error("invalid token")).type(MediaType.APPLICATION_JSON_TYPE).build();
}
if (!tokenManager.isTokenValid(session, realm, toIntrospect)) {
event.detail(Details.REASON, "stale token");
event.error(Errors.INVALID_TOKEN);
return Response.status(401).entity(TokenReviewResponseRepresentation.error("invalid token")).type(MediaType.APPLICATION_JSON_TYPE).build();
}
UserModel user = tokenManager.extractUser(session, realm, toIntrospect);
if (toIntrospect.getAudience() == null || toIntrospect.getAudience().length == 0) {
event.detail(Details.REASON, "token audience is missing");
event.error(Errors.INVALID_CLIENT);
return Response.status(401).entity(TokenReviewResponseRepresentation.error("invalid client")).type(MediaType.APPLICATION_JSON_TYPE).build();
}
ClientModel client = session.realms().getClientByClientId(toIntrospect.getIssuedFor(), realm);
TokenReviewResponseRepresentation success = TokenReviewResponseRepresentation.success();
TokenReviewResponseRepresentation.Status.User userRep = success.getStatus().getUser();
userRep.setUid(user.getId());
userRep.setUsername(user.getUsername());
// todo should scope this information to avoid leaking info
// should only display what is allowed for client
for (GroupModel group : user.getGroups()) {
addTree(userRep, group);
}
event.success();
return Response.ok(success, MediaType.APPLICATION_JSON).build();
}
public static void addTree(TokenReviewResponseRepresentation.Status.User user, GroupModel group) {
if (group.getParentId() != null) {
addTree(user, group.getParent());
}
String groupPath = buildOsinGroupPath(group);
if (!user.getGroups().contains(groupPath)) user.getGroups().add(groupPath);
}
public static void buildOsinGroupPath(StringBuilder sb, GroupModel group) {
if (group.getParent() != null) {
buildOsinGroupPath(sb, group.getParent());
sb.append(':');
}
sb.append(group.getName());
}
public static String buildOsinGroupPath(GroupModel group) {
StringBuilder sb = new StringBuilder();
buildOsinGroupPath(sb, group);
return sb.toString();
}
}
@@ -0,0 +1,123 @@
/*
* 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.protocol.kubernetes;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TokenReviewRequestRepresentation implements Serializable {
public static TokenReviewRequestRepresentation create(String token) {
TokenReviewRequestRepresentation rep = new TokenReviewRequestRepresentation();
rep.setSpec(new Spec());
rep.getSpec().setToken(token);
return rep;
}
@JsonProperty("apiVersion")
protected String apiVersion = "authentication.k8s.io/v1beta1";
@JsonProperty("kind")
protected String kind = "TokenReview";
protected Map<String, Object> otherClaims = new HashMap<>();
public static class Spec implements Serializable {
@JsonProperty("token")
protected String token;
protected Map<String, Object> otherClaims = new HashMap<>();
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
/**
* This is a map of any other claims and data that might be in the request. Don't know what Openshift will add in future
*
* @return
*/
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
}
public static TokenReviewRequestRepresentation build(String token) {
TokenReviewRequestRepresentation request = new TokenReviewRequestRepresentation();
request.setSpec(new Spec());
request.getSpec().setToken(token);
return request;
}
@JsonProperty("spec")
protected Spec spec;
public String getApiVersion() {
return apiVersion;
}
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
public String getKind() {
return kind;
}
public void setKind(String kind) {
this.kind = kind;
}
public Spec getSpec() {
return spec;
}
public void setSpec(Spec spec) {
this.spec = spec;
}
/**
* This is a map of any other claims and data that might be in the request. Don't know what Openshift will add in future
*
* @return
*/
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
}
@@ -0,0 +1,208 @@
/*
* 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.protocol.kubernetes;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TokenReviewResponseRepresentation implements Serializable {
@JsonProperty("apiVersion")
protected String apiVersion = "authentication.k8s.io/v1beta1";
@JsonProperty("kind")
protected String kind = "TokenReview";
protected Map<String, Object> otherClaims = new HashMap<>();
public static class Status implements Serializable {
@JsonProperty("authenticated")
protected boolean authenticated;
protected Map<String, Object> otherClaims = new HashMap<>();
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
public static class User implements Serializable {
@JsonProperty("username")
protected String username;
@JsonProperty("uid")
protected String uid;
@JsonProperty("groups")
protected Set<String> groups = new HashSet<>();
public static class Extra implements Serializable {
protected Map<String, Object> data = new HashMap<>();
@JsonAnyGetter
public Map<String, Object> getData() {
return data;
}
@JsonAnySetter
public void setData(String name, Object value) {
data.put(name, value);
}
}
@JsonProperty("extra")
protected Extra extra;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public Set<String> getGroups() {
return groups;
}
public void setGroups(Set<String> groups) {
this.groups = groups;
}
public Extra getExtra() {
return extra;
}
public void setExtra(Extra extra) {
this.extra = extra;
}
public void putExtra(String key, Object value) {
if (extra == null) extra = new Extra();
extra.data.put(key, value);
}
}
@JsonProperty("user")
protected User user;
@JsonProperty("error")
protected String error;
public boolean isAuthenticated() {
return authenticated;
}
public void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
@JsonProperty("status")
protected Status status = new Status();
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public String getApiVersion() {
return apiVersion;
}
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
public String getKind() {
return kind;
}
public void setKind(String kind) {
this.kind = kind;
}
/**
* This is a map of any other claims and data that might be in the request. Don't know what Openshift will add in future
*
* @return
*/
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
public static TokenReviewResponseRepresentation error(String error) {
TokenReviewResponseRepresentation response = new TokenReviewResponseRepresentation();
response.getStatus().setError(error);
return response;
}
public static TokenReviewResponseRepresentation success() {
TokenReviewResponseRepresentation response = new TokenReviewResponseRepresentation();
response.getStatus().setAuthenticated(true);
response.getStatus().setUser(new Status.User());
return response;
}
}
@@ -63,9 +63,9 @@ import java.util.List;
*/
public class OIDCLoginProtocolService {
private RealmModel realm;
private TokenManager tokenManager;
private EventBuilder event;
protected RealmModel realm;
protected TokenManager tokenManager;
protected EventBuilder event;
@Context
private UriInfo uriInfo;
@@ -302,6 +302,67 @@ public class TokenManager {
return new RefreshResult(res, TokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType()));
}
public UserModel extractUser(KeycloakSession session, RealmModel realm, AccessToken token) {
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
if (client == null || !client.isEnabled() || token.getIssuedAt() < client.getNotBefore()) {
return null;
}
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
if (AuthenticationManager.isSessionValid(realm, userSession)) {
return userSession.getUser();
}
userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
return userSession.getUser();
}
return null;
}
public String getRequiredOAuthScope(KeycloakSession session, RealmModel realm, AccessToken token) {
UserSessionModel userSession = null;
boolean offline = TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType());
ClientModel client = extractClient(realm, token);
if (client == null) return null;
if (offline) {
UserSessionManager sessionManager = new UserSessionManager(session);
userSession = sessionManager.findOfflineUserSession(realm, token.getSessionState());
} else {
// Find userSession regularly for online tokens
userSession = session.sessions().getUserSession(realm, token.getSessionState());
}
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
// Can theoretically happen in cross-dc environment. Try to see if userSession with our client is available in remoteCache
if (clientSession == null) {
userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSession.getId(), offline, client.getId());
if (userSession != null) {
clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
}
}
if (clientSession != null) {
return clientSession.getNote(OAuth2Constants.SCOPE);
}
return null;
}
public ClientModel extractClient(RealmModel realm, AccessToken token) {
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
if (client == null || !client.isEnabled() || token.getIssuedAt() < client.getNotBefore()) {
return null;
}
return client;
}
private void validateTokenReuse(KeycloakSession session, RealmModel realm, RefreshToken refreshToken,
TokenValidation validation) throws OAuthErrorException {
if (realm.isRevokeRefreshToken()) {
@@ -115,9 +115,9 @@ import java.security.MessageDigest;
public class TokenEndpoint {
private static final Logger logger = Logger.getLogger(TokenEndpoint.class);
private MultivaluedMap<String, String> formParams;
private ClientModel client;
private Map<String, String> clientAuthAttributes;
protected MultivaluedMap<String, String> formParams;
protected ClientModel client;
protected Map<String, String> clientAuthAttributes;
private enum Action {
AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS, TOKEN_EXCHANGE, PERMISSION
@@ -127,32 +127,32 @@ public class TokenEndpoint {
private static final Pattern VALID_CODE_VERIFIER_PATTERN = Pattern.compile("^[0-9a-zA-Z\\-\\.~_]+$");
@Context
private KeycloakSession session;
protected KeycloakSession session;
@Context
private HttpRequest request;
protected HttpRequest request;
@Context
private HttpResponse httpResponse;
protected HttpResponse httpResponse;
@Context
private HttpHeaders headers;
protected HttpHeaders headers;
@Context
private UriInfo uriInfo;
protected UriInfo uriInfo;
@Context
private ClientConnection clientConnection;
protected ClientConnection clientConnection;
private final TokenManager tokenManager;
private final RealmModel realm;
private final EventBuilder event;
protected final TokenManager tokenManager;
protected final RealmModel realm;
protected final EventBuilder event;
private Action action;
protected Action action;
private String grantType;
protected String grantType;
private Cors cors;
protected Cors cors;
public TokenEndpoint(TokenManager tokenManager, RealmModel realm, EventBuilder event) {
this.tokenManager = tokenManager;
@@ -210,19 +210,19 @@ public class TokenEndpoint {
return Cors.add(request, Response.ok()).auth().preflight().allowedMethods("POST", "OPTIONS").build();
}
private void checkSsl() {
protected void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
throw new CorsErrorResponseException(cors.allowAllOrigins(), OAuthErrorException.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN);
}
}
private void checkRealm() {
protected void checkRealm() {
if (!realm.isEnabled()) {
throw new CorsErrorResponseException(cors.allowAllOrigins(), "access_denied", "Realm not enabled", Response.Status.FORBIDDEN);
}
}
private void checkClient() {
protected void checkClient() {
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event);
client = clientAuth.getClient();
clientAuthAttributes = clientAuth.getClientAuthAttributes();
@@ -236,7 +236,7 @@ public class TokenEndpoint {
}
private void checkGrantType() {
protected void checkGrantType() {
if (grantType == null) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing form parameter: " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
}
@@ -0,0 +1,112 @@
/*
* 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.protocol.openshift;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.util.UriUtils;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ServicesLogger;
import java.util.*;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OpenshiftLoginProtocolFactory extends OIDCLoginProtocolFactory{
private static final Logger logger = Logger.getLogger(OpenshiftLoginProtocolFactory.class);
public static final String LOGIN_PROTOCOL = "openshift";
@Override
public Map<String, ProtocolMapperModel> getBuiltinMappers() {
return Collections.EMPTY_MAP;
}
@Override
protected void createDefaultClientScopesImpl(RealmModel newRealm) {
}
@Override
protected void addDefaults(ClientModel client) {
// OIDC will add defaults
}
@Override
public Object createProtocolEndpoint(RealmModel realm, EventBuilder event) {
return new OpenshiftProtocolEndpoint(realm, event);
}
@Override
public String getId() {
return LOGIN_PROTOCOL;
}
@Override
public void setupClientDefaults(ClientRepresentation rep, ClientModel newClient) {
if (rep.getRootUrl() != null && (rep.getRedirectUris() == null || rep.getRedirectUris().isEmpty())) {
String root = rep.getRootUrl();
if (root.endsWith("/")) root = root + "*";
else root = root + "/*";
newClient.addRedirectUri(root);
Set<String> origins = new HashSet<String>();
String origin = UriUtils.getOrigin(root);
logger.debugv("adding default client origin: {0}" , origin);
origins.add(origin);
newClient.setWebOrigins(origins);
}
if (rep.isBearerOnly() == null
&& rep.isPublicClient() == null) {
newClient.setPublicClient(true);
}
if (rep.isBearerOnly() == null) newClient.setBearerOnly(false);
// Backwards compatibility only
if (rep.isDirectGrantsOnly() != null) {
ServicesLogger.LOGGER.usingDeprecatedDirectGrantsOnly();
newClient.setStandardFlowEnabled(!rep.isDirectGrantsOnly());
newClient.setDirectAccessGrantsEnabled(rep.isDirectGrantsOnly());
} else {
if (rep.isStandardFlowEnabled() == null) newClient.setStandardFlowEnabled(true);
if (rep.isDirectAccessGrantsEnabled() == null) newClient.setDirectAccessGrantsEnabled(true);
}
if (rep.isImplicitFlowEnabled() == null) newClient.setImplicitFlowEnabled(true);
if (rep.isPublicClient() == null) newClient.setPublicClient(true);
if (rep.isFrontchannelLogout() == null) newClient.setFrontchannelLogout(false);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
}
@@ -0,0 +1,101 @@
/*
* 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.protocol.openshift;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.services.resources.RealmsResource;
import javax.ws.rs.Path;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OpenshiftProtocolEndpoint extends OIDCLoginProtocolService {
public OpenshiftProtocolEndpoint(RealmModel realm, EventBuilder event) {
super(realm, event);
}
/**
* Token endpoint
*/
@Path("token")
public Object token() {
OpenshiftTokenEndpoint endpoint = new OpenshiftTokenEndpoint(tokenManager, realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
return endpoint;
}
public static UriBuilder tokenServiceBaseUrl(UriInfo uriInfo) {
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return tokenServiceBaseUrl(baseUriBuilder);
}
public static UriBuilder tokenServiceBaseUrl(UriBuilder baseUriBuilder) {
return baseUriBuilder.path(RealmsResource.class).path("{realm}/protocol/" + OpenshiftLoginProtocolFactory.LOGIN_PROTOCOL);
}
public static UriBuilder authUrl(UriInfo uriInfo) {
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return authUrl(baseUriBuilder);
}
public static UriBuilder authUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "auth");
}
public static UriBuilder tokenUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "token");
}
public static UriBuilder certsUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "certs");
}
public static UriBuilder userInfoUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "issueUserInfo");
}
public static UriBuilder tokenIntrospectionUrl(UriBuilder baseUriBuilder) {
return tokenUrl(baseUriBuilder).path(TokenEndpoint.class, "introspect");
}
public static UriBuilder tokenReviewnUrl(UriBuilder baseUriBuilder) {
return tokenUrl(baseUriBuilder).path(OpenshiftTokenEndpoint.class, "tokenReview");
}
public static UriBuilder logoutUrl(UriInfo uriInfo) {
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return logoutUrl(baseUriBuilder);
}
public static UriBuilder logoutUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "logout");
}
}
@@ -0,0 +1,171 @@
/*
* 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.protocol.openshift;
import org.keycloak.RSATokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.openshift.clientstorage.OpenshiftClientModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.Urls;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.PublicKey;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OpenshiftTokenEndpoint extends TokenEndpoint {
public OpenshiftTokenEndpoint(TokenManager tokenManager, RealmModel realm, EventBuilder event) {
super(tokenManager, realm, event);
}
@Path("token-review")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response tokenReview(TokenReviewRequestRepresentation reviewRequest) throws Exception {
event.event(EventType.INTROSPECT_TOKEN);
checkSsl();
checkRealm();
AccessToken toIntrospect = null;
String token = reviewRequest.getSpec().getToken();
try {
RSATokenVerifier verifier = RSATokenVerifier.create(token)
.realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
if (publicKey == null) {
event.detail(Details.REASON, "invalid public key");
event.error(Errors.INVALID_TOKEN);
return Response.status(401).entity(TokenReviewResponseRepresentation.error("invalid token")).type(MediaType.APPLICATION_JSON_TYPE).build();
} else {
verifier.publicKey(publicKey);
verifier.verify();
toIntrospect = verifier.getToken();
}
} catch (VerificationException e) {
event.detail(Details.REASON, "token verification failure");
event.error(Errors.INVALID_TOKEN);
return Response.status(401).entity(TokenReviewResponseRepresentation.error("invalid token")).type(MediaType.APPLICATION_JSON_TYPE).build();
}
if (!tokenManager.isTokenValid(session, realm, toIntrospect)) {
event.detail(Details.REASON, "stale token");
event.error(Errors.INVALID_TOKEN);
return Response.status(401).entity(TokenReviewResponseRepresentation.error("invalid token")).type(MediaType.APPLICATION_JSON_TYPE).build();
}
UserModel user = tokenManager.extractUser(session, realm, toIntrospect);
if (toIntrospect.getAudience() == null || toIntrospect.getAudience().length == 0) {
event.detail(Details.REASON, "token audience is missing");
event.error(Errors.INVALID_CLIENT);
return Response.status(401).entity(TokenReviewResponseRepresentation.error("invalid client")).type(MediaType.APPLICATION_JSON_TYPE).build();
}
ClientModel client = session.realms().getClientByClientId(toIntrospect.getAudience()[0], realm);
if (client == null) {
event.detail(Details.REASON, "invalid audience");
event.error(Errors.INVALID_CLIENT);
return Response.status(401).entity(TokenReviewResponseRepresentation.error("invalid client")).type(MediaType.APPLICATION_JSON_TYPE).build();
}
TokenReviewResponseRepresentation success = TokenReviewResponseRepresentation.success();
TokenReviewResponseRepresentation.Status.User userRep = success.getStatus().getUser();
userRep.setUid(user.getId());
userRep.setUsername(user.getUsername());
String scopeParam = tokenManager.getRequiredOAuthScope(session, realm, toIntrospect);
List<String> scopes = new LinkedList<>();
if (scopeParam != null) {
for (String scope : scopeParam.split("\\s+")) {
scopes.add(scope);
}
}
if (client instanceof OpenshiftClientModel) {
Set<String> failedScopes = ((OpenshiftClientModel)client).validateRequestedScope(scopes);
if (failedScopes != null && !failedScopes.isEmpty()) {
StringBuilder builder = new StringBuilder();
for (String failed : failedScopes) builder.append(" ").append(failed);
event.detail(Details.REASON, builder.toString());
event.error(Errors.INVALID_SCOPE);
return Response.status(401).entity(TokenReviewResponseRepresentation.error(Errors.INVALID_SCOPE)).type(MediaType.APPLICATION_JSON_TYPE).build();
}
if (!scopes.isEmpty()) userRep.putExtra("scopes.authorization.openshift.io", scopes);
}
// todo should scope this information to avoid leaking info
// should only display what is allowed for client
for (GroupModel group : user.getGroups()) {
addTree(userRep, group);
}
event.success();
return Response.ok(success, MediaType.APPLICATION_JSON).build();
}
public static void addTree(TokenReviewResponseRepresentation.Status.User user, GroupModel group) {
if (group.getParentId() != null) {
addTree(user, group.getParent());
}
String groupPath = buildOsinGroupPath(group);
if (!user.getGroups().contains(groupPath)) user.getGroups().add(groupPath);
}
public static void buildOsinGroupPath(StringBuilder sb, GroupModel group) {
if (group.getParent() != null) {
buildOsinGroupPath(sb, group.getParent());
sb.append(':');
}
sb.append(group.getName());
}
public static String buildOsinGroupPath(GroupModel group) {
StringBuilder sb = new StringBuilder();
buildOsinGroupPath(sb, group);
return sb.toString();
}
}
@@ -0,0 +1,129 @@
/*
* 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.protocol.openshift;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.keycloak.common.util.Time;
import org.keycloak.json.StringOrArrayDeserializer;
import org.keycloak.json.StringOrArraySerializer;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TokenReviewRequestRepresentation implements Serializable {
public static TokenReviewRequestRepresentation create(String token) {
TokenReviewRequestRepresentation rep = new TokenReviewRequestRepresentation();
rep.setSpec(new Spec());
rep.getSpec().setToken(token);
return rep;
}
@JsonProperty("apiVersion")
protected String apiVersion = "authentication.k8s.io/v1beta1";
@JsonProperty("kind")
protected String kind = "TokenReview";
protected Map<String, Object> otherClaims = new HashMap<>();
public static class Spec implements Serializable {
@JsonProperty("token")
protected String token;
protected Map<String, Object> otherClaims = new HashMap<>();
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
/**
* This is a map of any other claims and data that might be in the request. Don't know what Openshift will add in future
*
* @return
*/
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
}
public static TokenReviewRequestRepresentation build(String token) {
TokenReviewRequestRepresentation request = new TokenReviewRequestRepresentation();
request.setSpec(new Spec());
request.getSpec().setToken(token);
return request;
}
@JsonProperty("spec")
protected Spec spec;
public String getApiVersion() {
return apiVersion;
}
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
public String getKind() {
return kind;
}
public void setKind(String kind) {
this.kind = kind;
}
public Spec getSpec() {
return spec;
}
public void setSpec(Spec spec) {
this.spec = spec;
}
/**
* This is a map of any other claims and data that might be in the request. Don't know what Openshift will add in future
*
* @return
*/
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
}
@@ -0,0 +1,208 @@
/*
* 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.protocol.openshift;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TokenReviewResponseRepresentation implements Serializable {
@JsonProperty("apiVersion")
protected String apiVersion = "authentication.k8s.io/v1beta1";
@JsonProperty("kind")
protected String kind = "TokenReview";
protected Map<String, Object> otherClaims = new HashMap<>();
public static class Status implements Serializable {
@JsonProperty("authenticated")
protected boolean authenticated;
protected Map<String, Object> otherClaims = new HashMap<>();
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
public static class User implements Serializable {
@JsonProperty("username")
protected String username;
@JsonProperty("uid")
protected String uid;
@JsonProperty("groups")
protected Set<String> groups = new HashSet<>();
public static class Extra implements Serializable {
protected Map<String, Object> data = new HashMap<>();
@JsonAnyGetter
public Map<String, Object> getData() {
return data;
}
@JsonAnySetter
public void setData(String name, Object value) {
data.put(name, value);
}
}
@JsonProperty("extra")
protected Extra extra;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public Set<String> getGroups() {
return groups;
}
public void setGroups(Set<String> groups) {
this.groups = groups;
}
public Extra getExtra() {
return extra;
}
public void setExtra(Extra extra) {
this.extra = extra;
}
public void putExtra(String key, Object value) {
if (extra == null) extra = new Extra();
extra.data.put(key, value);
}
}
@JsonProperty("user")
protected User user;
@JsonProperty("error")
protected String error;
public boolean isAuthenticated() {
return authenticated;
}
public void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
@JsonProperty("status")
protected Status status = new Status();
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public String getApiVersion() {
return apiVersion;
}
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
public String getKind() {
return kind;
}
public void setKind(String kind) {
this.kind = kind;
}
/**
* This is a map of any other claims and data that might be in the request. Don't know what Openshift will add in future
*
* @return
*/
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
public static TokenReviewResponseRepresentation error(String error) {
TokenReviewResponseRepresentation response = new TokenReviewResponseRepresentation();
response.getStatus().setError(error);
return response;
}
public static TokenReviewResponseRepresentation success() {
TokenReviewResponseRepresentation response = new TokenReviewResponseRepresentation();
response.getStatus().setAuthenticated(true);
response.getStatus().setUser(new Status.User());
return response;
}
}
@@ -0,0 +1,30 @@
/*
* 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.protocol.openshift.clientstorage;
import org.keycloak.models.ClientModel;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface OpenshiftClientModel extends ClientModel {
Set<String> validateRequestedScope(List<String> requestedScopes);
}
@@ -0,0 +1,60 @@
/*
* 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.protocol.openshift.clientstorage;
import org.keycloak.component.ComponentModel;
import org.keycloak.storage.client.ClientStorageProviderModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OpenshiftClientStorageModel extends ClientStorageProviderModel {
protected transient String token;
protected transient String openshiftUri;
public OpenshiftClientStorageModel() {
}
public OpenshiftClientStorageModel(ComponentModel copy) {
super(copy);
}
public String getToken() {
if (token == null) {
token = getConfig().getFirst(OpenshiftClientStorageProviderFactory.ACCESS_TOKEN);
}
return token;
}
public void setToken(String token) {
this.token = token;
getConfig().putSingle(OpenshiftClientStorageProviderFactory.ACCESS_TOKEN, token);
}
public String getOpenshiftUri() {
if (openshiftUri == null) {
openshiftUri = getConfig().getFirst(OpenshiftClientStorageProviderFactory.OPENSHIFT_URI);
}
return openshiftUri;
}
public void setOpenshiftUri(String openshiftUri) {
this.openshiftUri = openshiftUri;
getConfig().putSingle(OpenshiftClientStorageProviderFactory.OPENSHIFT_URI, openshiftUri);
}
}
@@ -0,0 +1,107 @@
/*
* 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.protocol.openshift.clientstorage;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.openshift.connections.rest.OpenshiftClient;
import org.keycloak.protocol.openshift.connections.rest.api.v1.ServiceAccounts;
import org.keycloak.protocol.openshift.connections.rest.apis.oauth.OAuthClients;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.client.ClientLookupProvider;
import org.keycloak.storage.client.ClientStorageProvider;
import org.keycloak.storage.client.ClientStorageProviderModel;
import javax.ws.rs.NotFoundException;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OpenshiftClientStorageProvider implements ClientStorageProvider, ClientLookupProvider {
protected KeycloakSession session;
protected OpenshiftClientStorageModel component;
protected OpenshiftClient openshiftClient;
public OpenshiftClientStorageProvider(KeycloakSession session, ClientStorageProviderModel component) {
this.session = session;
this.component = new OpenshiftClientStorageModel(component);
}
public OpenshiftClientStorageModel getComponent() {
return component;
}
@Override
public void close() {
if (openshiftClient != null) openshiftClient.close();
}
protected void initClient() {
if (openshiftClient != null) return;
openshiftClient = OpenshiftClient.instance(component.getOpenshiftUri(), component.getToken());
}
public OpenshiftClient getOpenshiftClient() {
initClient();
return openshiftClient;
}
@Override
public ClientModel getClientById(String id, RealmModel realm) {
StorageId storageId = new StorageId(id);
if (!storageId.getProviderId().equals(component.getId())) return null;
String clientId = storageId.getExternalId();
return getClientByClientId(clientId, realm);
}
@Override
public ClientModel getClientByClientId(String clientId, RealmModel realm) {
initClient();
clientId = clientId.trim();
Matcher matcher = OpenshiftSAClientAdapter.SERVICE_ACCOUNT_PATTERN.matcher(clientId);
if (matcher.matches()) {
String namespace = matcher.group(1);
String name = matcher.group(2);
try {
ServiceAccounts.ServiceAccountRepresentation rep = openshiftClient.api().namespace(namespace).serviceAccounts().get(name);
return new OpenshiftSAClientAdapter(session, realm, clientId, rep, this);
} catch (NotFoundException nfe) {
if (nfe.getResponse() != null) nfe.getResponse().close();
return null;
}
} else {
OAuthClients.OAuthClientRepresentation client = null;
try {
client = openshiftClient.apis().oauth().clients().get(clientId);
return new OpenshiftOAuthClientAdapter(session, realm, component, client);
} catch (NotFoundException nfe) {
if (nfe.getResponse() != null) nfe.getResponse().close();
return null;
}
}
}
}
@@ -0,0 +1,86 @@
/*
* 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.protocol.openshift.clientstorage;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.CacheableStorageProviderModel;
import org.keycloak.storage.client.ClientStorageProviderFactory;
import org.keycloak.storage.client.ClientStorageProviderModel;
import java.util.List;
import static org.keycloak.storage.CacheableStorageProviderModel.CACHE_POLICY;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OpenshiftClientStorageProviderFactory implements ClientStorageProviderFactory<OpenshiftClientStorageProvider> {
@Override
public OpenshiftClientStorageProvider create(KeycloakSession session, ComponentModel model) {
return new OpenshiftClientStorageProvider(session, new ClientStorageProviderModel(model));
}
public static final String PROVIDER_ID = "openshift-oauth-clients";
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getHelpText() {
return "Openshift OAuth Client Adapter";
}
protected static final List<ProviderConfigProperty> CONFIG_PROPERTIES;
public static final String ACCESS_TOKEN = "access_token";
public static final String OPENSHIFT_URI = "openshift_uri";
static {
CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
.property().name(ACCESS_TOKEN)
.type(ProviderConfigProperty.STRING_TYPE)
.label("Access Token")
.helpText("Bearer token that will be used to invoke on Openshift api server. Must have privilege to lookup oauth clients, service accounts, and invoke on token review interface")
.add()
.property().name(OPENSHIFT_URI)
.type(ProviderConfigProperty.STRING_TYPE)
.label("Openshift URL")
.helpText("Openshift api server URL base endpoint.")
.add()
.build();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
config.getConfig().putSingle(CACHE_POLICY, CacheableStorageProviderModel.CachePolicy.NO_CACHE.name());
}
}
@@ -0,0 +1,303 @@
/*
* 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.protocol.openshift.clientstorage;
import org.keycloak.models.*;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.openshift.connections.rest.apis.oauth.OAuthClients;
import org.keycloak.storage.client.AbstractReadOnlyClientStorageAdapter;
import org.keycloak.storage.client.ClientStorageProviderModel;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import static org.keycloak.protocol.openshift.clientstorage.OpenshiftSAClientAdapter.ROLE_SCOPE_PATTERN;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OpenshiftOAuthClientAdapter extends AbstractReadOnlyClientStorageAdapter implements OpenshiftClientModel {
protected OAuthClients.OAuthClientRepresentation client;
public OpenshiftOAuthClientAdapter(KeycloakSession session, RealmModel realm, ClientStorageProviderModel component, OAuthClients.OAuthClientRepresentation client) {
super(session, realm, component);
this.client = client;
}
@Override
public String getClientId() {
return client.getName();
}
@Override
public String getName() {
return client.getName();
}
@Override
public String getDescription() {
return "";
}
@Override
public boolean isEnabled() {
return true;
}
private static Set<String> origins = new HashSet<>();
static {
origins.add("+");
}
@Override
public Set<String> getWebOrigins() {
return origins;
}
@Override
public Set<String> getRedirectUris() {
if (client.getRedirectURIs() != null) {
Set<String> newSet = new HashSet<>();
for (String uri : client.getRedirectURIs()) {
if (!uri.endsWith("/")) uri += "/";
uri += "*";
newSet.add(uri);
}
return newSet;
}
return client.getRedirectURIs();
}
@Override
public String getManagementUrl() {
return null;
}
@Override
public String getRootUrl() {
return null;
}
@Override
public String getBaseUrl() {
return null;
}
@Override
public boolean isBearerOnly() {
return false;
}
@Override
public int getNodeReRegistrationTimeout() {
return 0;
}
@Override
public String getClientAuthenticatorType() {
return null;
}
@Override
public boolean validateSecret(String secret) {
return client.getSecret().equals(secret);
}
@Override
public String getSecret() {
return client.getSecret();
}
@Override
public String getRegistrationToken() {
return null;
}
@Override
public String getProtocol() {
return OIDCLoginProtocol.LOGIN_PROTOCOL;
}
@Override
public String getAttribute(String name) {
return null;
}
@Override
public Map<String, String> getAttributes() {
return Collections.EMPTY_MAP;
}
@Override
public String getAuthenticationFlowBindingOverride(String binding) {
if (!AuthenticationFlowBindings.BROWSER_BINDING.equals(binding)) return null;
if (client.isRespondWithChallenges()) {
AuthenticationFlowModel model = realm.getFlowByAlias(DefaultAuthenticationFlows.OPENSHIFT_CHALLENGE_FLOW);
if (model != null) return model.getId();
}
return null;
}
@Override
public Map<String, String> getAuthenticationFlowBindingOverrides() {
if (client.isRespondWithChallenges()) {
AuthenticationFlowModel model = realm.getFlowByAlias(DefaultAuthenticationFlows.OPENSHIFT_CHALLENGE_FLOW);
if (model != null) {
Map<String, String> map = new HashMap<>();
map.put(AuthenticationFlowBindings.BROWSER_BINDING, model.getId());
return map;
}
}
return Collections.EMPTY_MAP;
}
@Override
public boolean isFrontchannelLogout() {
return false;
}
@Override
public boolean isPublicClient() {
return client.getSecret() == null;
}
@Override
public boolean isConsentRequired() {
return client.getGrantMethod() == null ? false : client.getGrantMethod().equalsIgnoreCase("prompt");
}
@Override
public boolean isStandardFlowEnabled() {
return true;
}
@Override
public boolean isImplicitFlowEnabled() {
return true;
}
@Override
public boolean isDirectAccessGrantsEnabled() {
return true;
}
@Override
public boolean isServiceAccountsEnabled() {
return false;
}
@Override
public int getNotBefore() {
return 0;
}
@Override
public Set<ProtocolMapperModel> getProtocolMappers() {
return Collections.EMPTY_SET;
}
@Override
public ProtocolMapperModel getProtocolMapperById(String id) {
return null;
}
@Override
public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) {
return null;
}
@Override
public boolean isFullScopeAllowed() {
return false;
}
@Override
public Set<RoleModel> getScopeMappings() {
return Collections.EMPTY_SET;
}
@Override
public Set<RoleModel> getRealmScopeMappings() {
return Collections.EMPTY_SET;
}
@Override
public boolean hasScope(RoleModel role) {
return false;
}
@Override
public Set<String> validateRequestedScope(List<String> requestedScopes) {
Set<String> failed = new HashSet<>();
for (String requested : requestedScopes) {
if (requested.equals("openid")) continue;
if (client.getLiteralScopeRestrictions() != null && client.getLiteralScopeRestrictions().contains(requested)) continue;
if (client.getClusterRoleRestrictions() == null || client.getClusterRoleRestrictions().isEmpty()) {
failed.add(requested);
continue;
}
Matcher m = ROLE_SCOPE_PATTERN.matcher(requested);
if (!m.matches()) {
failed.add(requested);
continue;
}
boolean found = false;
String role = m.group(1);
String namespace = m.group(2);
String escalationString = m.group(3);
boolean escalation = escalationString != null && escalationString.trim().equals(":!");
for (OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction restriction : client.getClusterRoleRestrictions()) {
if (restriction.getNamespaces().contains("*") || restriction.getNamespaces().contains(namespace)) {
if (restriction.getRoleNames().contains("*") || restriction.getRoleNames().contains(role)) {
if (!escalation || restriction.isAllowEscalation()) {
found = true;
}
break;
}
}
}
if (!found) failed.add(requested);
}
return failed;
}
@Override
public Map<String, ClientScopeModel> getClientScopes(boolean defaultScope, boolean filterByProtocol) {
return Collections.EMPTY_MAP;
}
@Override
public boolean isDisplayOnConsentScreen() {
return false;
}
@Override
public String getConsentScreenText() {
return "";
}
}
@@ -0,0 +1,293 @@
/*
* 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.protocol.openshift.clientstorage;
import org.keycloak.models.*;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.openshift.TokenReviewRequestRepresentation;
import org.keycloak.protocol.openshift.TokenReviewResponseRepresentation;
import org.keycloak.protocol.openshift.connections.rest.api.v1.ServiceAccounts;
import org.keycloak.protocol.openshift.connections.rest.apis.oauth.OAuthClients;
import org.keycloak.storage.client.AbstractReadOnlyClientStorageAdapter;
import org.keycloak.storage.client.ClientStorageProviderModel;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OpenshiftSAClientAdapter extends AbstractReadOnlyClientStorageAdapter implements OpenshiftClientModel {
public static final Pattern ROLE_SCOPE_PATTERN = Pattern.compile("role:([^:]+):([^:!]+)(:[!])?");
public static final Pattern SERVICE_ACCOUNT_PATTERN = Pattern.compile("system:serviceaccount:([^:]+):([^:]+)");
protected final ServiceAccounts.ServiceAccountRepresentation serviceAccount;
protected final OpenshiftClientStorageProvider provider;
protected final String clientId;
public static final Set<String> ALLOWED_SCOPES = new HashSet<>();
static {
ALLOWED_SCOPES.add("user:info");
ALLOWED_SCOPES.add("user:check-access");
}
public OpenshiftSAClientAdapter(KeycloakSession session, RealmModel realm, String clientId, ServiceAccounts.ServiceAccountRepresentation serviceAccount, OpenshiftClientStorageProvider provider) {
super(session, realm, provider.getComponent());
this.serviceAccount = serviceAccount;
this.provider = provider;
this.clientId = clientId;
}
@Override
public String getClientId() {
return clientId;
}
@Override
public String getName() {
return clientId;
}
@Override
public String getDescription() {
return "";
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Set<String> getWebOrigins() {
return Collections.EMPTY_SET;
}
@Override
public Set<String> getRedirectUris() {
return serviceAccount.getOauthRedirectUris();
}
@Override
public String getManagementUrl() {
return null;
}
@Override
public String getRootUrl() {
return null;
}
@Override
public String getBaseUrl() {
return null;
}
@Override
public boolean isBearerOnly() {
return false;
}
@Override
public int getNodeReRegistrationTimeout() {
return 0;
}
@Override
public String getClientAuthenticatorType() {
return null;
}
@Override
public boolean validateSecret(String secret) {
TokenReviewRequestRepresentation request = TokenReviewRequestRepresentation.create(secret);
Response response = provider.getOpenshiftClient().apis().kubernetesAuthentication().tokenReview().review(request);
if (response.getStatus() < 200 || response.getStatus() > 204) {
return false;
}
TokenReviewResponseRepresentation review = response.readEntity(TokenReviewResponseRepresentation.class);
response.close();
if (!review.getStatus().isAuthenticated()) return false;
return clientId.equals(review.getStatus().getUser().getUsername());
}
@Override
public String getSecret() {
return "";
}
@Override
public String getRegistrationToken() {
return null;
}
@Override
public String getProtocol() {
return OIDCLoginProtocol.LOGIN_PROTOCOL;
}
@Override
public String getAttribute(String name) {
return null;
}
@Override
public Map<String, String> getAttributes() {
return Collections.EMPTY_MAP;
}
@Override
public String getAuthenticationFlowBindingOverride(String binding) {
if (!AuthenticationFlowBindings.BROWSER_BINDING.equals(binding)) return null;
if (serviceAccount.oauthWantChallenges()) {
AuthenticationFlowModel model = realm.getFlowByAlias(DefaultAuthenticationFlows.OPENSHIFT_CHALLENGE_FLOW);
if (model != null) return model.getId();
}
return null;
}
@Override
public Map<String, String> getAuthenticationFlowBindingOverrides() {
if (serviceAccount.oauthWantChallenges()) {
AuthenticationFlowModel model = realm.getFlowByAlias(DefaultAuthenticationFlows.OPENSHIFT_CHALLENGE_FLOW);
if (model != null) {
Map<String, String> map = new HashMap<>();
map.put(AuthenticationFlowBindings.BROWSER_BINDING, model.getId());
return map;
}
}
return Collections.EMPTY_MAP;
}
@Override
public boolean isFrontchannelLogout() {
return false;
}
@Override
public boolean isPublicClient() {
return false;
}
@Override
public boolean isConsentRequired() {
return false;
}
@Override
public boolean isStandardFlowEnabled() {
return true;
}
@Override
public boolean isImplicitFlowEnabled() {
return true;
}
@Override
public boolean isDirectAccessGrantsEnabled() {
return true;
}
@Override
public boolean isServiceAccountsEnabled() {
return false;
}
@Override
public int getNotBefore() {
return 0;
}
@Override
public Set<ProtocolMapperModel> getProtocolMappers() {
return Collections.EMPTY_SET;
}
@Override
public ProtocolMapperModel getProtocolMapperById(String id) {
return null;
}
@Override
public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) {
return null;
}
@Override
public boolean isFullScopeAllowed() {
return false;
}
@Override
public Set<RoleModel> getScopeMappings() {
return Collections.EMPTY_SET;
}
@Override
public Set<RoleModel> getRealmScopeMappings() {
return Collections.EMPTY_SET;
}
@Override
public boolean hasScope(RoleModel role) {
return false;
}
public Set<String> validateRequestedScope(List<String> requestedScopes) {
Set<String> failed = new HashSet<>();
for (String requested : requestedScopes) {
if (ALLOWED_SCOPES.contains(requested)) continue;
Matcher m = ROLE_SCOPE_PATTERN.matcher(requested);
if (!m.matches()) {
failed.add(requested);
continue;
}
// I think that it has to match namespace? Not sure.
String namespace = m.group(2);
if (!serviceAccount.getNamespace().equals(namespace)) {
failed.add(requested);
}
}
return failed;
}
@Override
public Map<String, ClientScopeModel> getClientScopes(boolean defaultScope, boolean filterByProtocol) {
return Collections.EMPTY_MAP;
}
@Override
public boolean isDisplayOnConsentScreen() {
return false;
}
@Override
public String getConsentScreenText() {
return "";
}
}
@@ -0,0 +1,51 @@
/*
* 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.protocol.openshift.connections.rest;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BaseRepresentation {
@JsonProperty("apiVersion")
protected String apiVersion;
@JsonProperty("kind")
protected String kind;
protected Map<String, Object> otherClaims = new HashMap<>();
public void setOtherClaims(Map<String, Object> otherClaims) {
this.otherClaims = otherClaims;
}
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
}
@@ -0,0 +1,42 @@
/*
* 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.protocol.openshift.connections.rest;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import java.io.IOException;
/**
* @author rodrigo.sasaki@icarros.com.br
*/
public class BearerAuthFilter implements ClientRequestFilter {
public static final String AUTH_HEADER_PREFIX = "Bearer ";
private final String tokenString;
public BearerAuthFilter(String tokenString) {
this.tokenString = tokenString;
}
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, AUTH_HEADER_PREFIX + tokenString);
}
}
@@ -0,0 +1,77 @@
/*
* 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.protocol.openshift.connections.rest;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class MetadataRepresentation {
@JsonProperty("name")
protected String name;
@JsonProperty("namespace")
protected String namespace;
@JsonProperty("annotations")
protected Map<String, String> annotations = new HashMap<>();
protected Map<String, Object> otherClaims = new HashMap<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public Map<String, String> getAnnotations() {
return annotations;
}
public void setAnnotations(Map<String, String> annotations) {
this.annotations = annotations;
}
public void setOtherClaims(Map<String, Object> otherClaims) {
this.otherClaims = otherClaims;
}
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
}
@@ -0,0 +1,62 @@
/*
* 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.protocol.openshift.connections.rest;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.keycloak.protocol.openshift.connections.rest.api.Api;
import org.keycloak.protocol.openshift.connections.rest.apis.Apis;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OpenshiftClient {
protected String token;
protected String baseUrl;
protected ResteasyClient httpClient;
private final ResteasyWebTarget target;
private final ResteasyWebTarget api;
private final ResteasyWebTarget apis;
protected OpenshiftClient(String baseUrl, String token, ResteasyClient httpClient) {
this.httpClient = httpClient;
this.token = token;
this.baseUrl = baseUrl;
target = httpClient.target(baseUrl);
target.register(new BearerAuthFilter(token));
api = target.path("api");
apis = target.path("apis");
}
public static OpenshiftClient instance(String baseUrl, String token) {
return new OpenshiftClient(baseUrl, token, new ResteasyClientBuilder().connectionPoolSize(1).disableTrustManager().build());
}
public Api api() {
return api.proxy(Api.class);
}
public Apis apis() {
return apis.proxy(Apis.class);
}
public void close() {
httpClient.close();
}
}
@@ -0,0 +1,32 @@
/*
* 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.protocol.openshift.connections.rest.api;
import org.keycloak.protocol.openshift.connections.rest.api.v1.Namespace;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface Api {
@Path("v1/namespaces/{namespace}")
Namespace namespace(@PathParam("namespace") String namespace);
}
@@ -0,0 +1,30 @@
/*
* 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.protocol.openshift.connections.rest.api.v1;
import javax.ws.rs.Path;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface Namespace {
@Path("serviceaccounts")
ServiceAccounts serviceAccounts();
@Path("secrets")
Secrets secrets();
}
@@ -0,0 +1,95 @@
/*
* 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.protocol.openshift.connections.rest.api.v1;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.common.util.Base64;
import org.keycloak.protocol.openshift.connections.rest.BaseRepresentation;
import org.keycloak.protocol.openshift.connections.rest.MetadataRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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 java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface Secrets {
public final String SERVICE_ACCOUNT_TOKEN="kubernetes.io/service-account-token";
class SecretRepresentation extends BaseRepresentation {
@JsonProperty("metadata")
protected MetadataRepresentation metadata = new MetadataRepresentation();
@JsonProperty("type")
protected String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@JsonIgnore
public boolean isServiceAccountToken() {
return SERVICE_ACCOUNT_TOKEN.equals(type);
}
@JsonIgnore
public String getToken() {
Object data = otherClaims.get("data");
if (data == null) return null;
Map<String, Object> dataMap = (Map<String, Object>)data;
Object token = dataMap.get("token");
if (token == null) return null;
try {
byte[] decoded = Base64.decode((String)token);
return new String(decoded, "UTF-8");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@GET
@Path("{secret}")
@Produces(MediaType.APPLICATION_JSON)
public SecretRepresentation get(@PathParam("secret") String secret);
@GET
@Path("{secret}")
@Produces(MediaType.APPLICATION_JSON)
public String getPretty(@PathParam("secret") String secret, @QueryParam("pretty") boolean pretty);
}
@@ -0,0 +1,164 @@
/*
* 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.protocol.openshift.connections.rest.api.v1;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.protocol.openshift.connections.rest.BaseRepresentation;
import org.keycloak.protocol.openshift.connections.rest.MetadataRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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 java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ServiceAccounts {
String SERVICEACCOUNTS_OPENSHIFT_IO_OAUTH_REDIRECTURI = "serviceaccounts.openshift.io/oauth-redirecturi";
String SERVICEACCOUNTS_OPENSHIFT_IO_OAUTH_WANT_CHALLENGES = "serviceaccounts.openshift.io/oauth-want-challenges";
class ServiceAccountRepresentation extends BaseRepresentation {
public ServiceAccountRepresentation() {
this.kind = "ServiceAccount";
this.apiVersion = "v1";
}
@JsonProperty("metadata")
protected MetadataRepresentation metadata = new MetadataRepresentation();
public MetadataRepresentation getMetadata() {
return metadata;
}
public void setMetadata(MetadataRepresentation metadata) {
this.metadata = metadata;
}
@JsonProperty("secrets")
protected List<Map<String, String>> secrets;
@JsonIgnore
public Set<String> getSecrets() {
if (secrets == null || secrets.isEmpty()) return Collections.EMPTY_SET;
Set<String> set = new HashSet<>();
for (Map<String, String> secret : secrets) {
set.add(secret.values().iterator().next());
}
return set;
}
@JsonIgnore
public String getName() {
return metadata.getName();
}
@JsonIgnore
public void setName(String name) {
metadata.setName(name);
}
@JsonIgnore
public String getNamespace() {
return metadata.getNamespace();
}
@JsonIgnore
public void setNamespace(String name) {
metadata.setNamespace(name);
}
@JsonIgnore
public Map<String, String> getAnnotations() {
return metadata.getAnnotations();
}
@JsonIgnore
public Set<String> getOauthRedirectUris() {
Set<String> redirects = new HashSet<>();
for (String key : getAnnotations().keySet()) {
if (key.startsWith(SERVICEACCOUNTS_OPENSHIFT_IO_OAUTH_REDIRECTURI)) {
redirects.add(getAnnotations().get(key));
}
}
return redirects;
}
@JsonIgnore
public void addRedirectUri(String uri) {
for (int i = 0; i < 100; i++) {
String key = SERVICEACCOUNTS_OPENSHIFT_IO_OAUTH_REDIRECTURI + "." + (i + 1);
if (getAnnotations().containsKey(key)) continue;
getAnnotations().put(key, uri);
break;
}
}
@JsonIgnore
public boolean oauthWantChallenges() {
String val = getAnnotations().get(SERVICEACCOUNTS_OPENSHIFT_IO_OAUTH_WANT_CHALLENGES);
if (val == null) return false;
return val.trim().equalsIgnoreCase("true");
}
@JsonIgnore
public void setOauthWantChallenges(boolean wantChallenges) {
getAnnotations().put(SERVICEACCOUNTS_OPENSHIFT_IO_OAUTH_WANT_CHALLENGES, Boolean.toString(wantChallenges));
}
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ServiceAccountRepresentation create(ServiceAccountRepresentation rep);
@GET
@Path("{sa-name}")
@Produces(MediaType.APPLICATION_JSON)
public ServiceAccountRepresentation get(@PathParam("sa-name") String name);
@DELETE
@Path("{sa-name}")
@Produces(MediaType.APPLICATION_JSON)
public Response delete(@PathParam("sa-name") String name);
}
@@ -0,0 +1,38 @@
/*
* 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.protocol.openshift.connections.rest.apis;
import org.keycloak.protocol.openshift.connections.rest.apis.kubernetes.KubernetesAuthentication;
import org.keycloak.protocol.openshift.connections.rest.apis.kubernetes.TokenReview;
import org.keycloak.protocol.openshift.connections.rest.apis.oauth.OpenshiftOAuth;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface Apis {
@Path("authentication.k8s.io/v1beta1")
KubernetesAuthentication kubernetesAuthentication();
@Path("oauth.openshift.io/v1")
OpenshiftOAuth oauth();
}
@@ -0,0 +1,28 @@
/*
* 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.protocol.openshift.connections.rest.apis.kubernetes;
import javax.ws.rs.Path;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface KubernetesAuthentication {
@Path("tokenreviews")
TokenReview tokenReview();
}
@@ -0,0 +1,43 @@
/*
* 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.protocol.openshift.connections.rest.apis.kubernetes;
import org.keycloak.protocol.openshift.TokenReviewRequestRepresentation;
import org.keycloak.protocol.openshift.TokenReviewResponseRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface TokenReview {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
Response review(TokenReviewRequestRepresentation token);
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
String reviewPretty(TokenReviewRequestRepresentation token, @QueryParam("pretty") boolean pretty);
}
@@ -0,0 +1,286 @@
/*
* 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.protocol.openshift.connections.rest.apis.oauth;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.protocol.openshift.connections.rest.BaseRepresentation;
import org.keycloak.protocol.openshift.connections.rest.MetadataRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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 java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface OAuthClients {
class OAuthClientRepresentation extends BaseRepresentation {
public static OAuthClientRepresentation create() {
OAuthClientRepresentation rep = new OAuthClientRepresentation();
rep.wasScopeRestrictionsParsed = true;
return rep;
}
protected OAuthClientRepresentation() {
}
@JsonProperty("metadata")
protected MetadataRepresentation metadata = new MetadataRepresentation();
public MetadataRepresentation getMetadata() {
return metadata;
}
public void setMetadata(MetadataRepresentation metadata) {
this.metadata = metadata;
}
@JsonIgnore
public String getName() {
return metadata.getName();
}
@JsonIgnore
public void setName(String name) {
metadata.setName(name);
}
@JsonProperty("respondWithChallenges")
protected boolean respondWithChallenges;
@JsonProperty("redirectURIs")
protected Set<String> redirectURIs;
@JsonProperty("grantMethod")
protected String grantMethod;
@JsonProperty("secret")
protected String secret;
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public boolean isRespondWithChallenges() {
return respondWithChallenges;
}
public void setRespondWithChallenges(boolean respondWithChallenges) {
this.respondWithChallenges = respondWithChallenges;
}
public Set<String> getRedirectURIs() {
return redirectURIs;
}
public void setRedirectURIs(Set<String> redirectURIs) {
this.redirectURIs = redirectURIs;
}
public void addRedirectURI(String redirect) {
if (this.redirectURIs == null) this.redirectURIs = new HashSet<>();
this.redirectURIs.add(redirect);
}
public String getGrantMethod() {
return grantMethod;
}
public void setGrantMethod(String grantMethod) {
this.grantMethod = grantMethod;
}
@JsonProperty("scopeRestrictions")
protected List<Object> scopeRestrictions;
@JsonIgnore
protected boolean wasScopeRestrictionsParsed;
@JsonIgnore
protected Set<String> literalScopeRestrictions;
@JsonIgnore
protected List<ClusterRoleRestriction> clusterRoleRestrictions;
@JsonIgnore
public Set<String> getLiteralScopeRestrictions() {
parseScopeRestrictions();
return literalScopeRestrictions;
}
@JsonIgnore
public List<ClusterRoleRestriction> getClusterRoleRestrictions() {
parseScopeRestrictions();
return clusterRoleRestrictions;
}
protected void parseScopeRestrictions() {
if (wasScopeRestrictionsParsed) return;
wasScopeRestrictionsParsed = true;
if (scopeRestrictions == null) return;
List<Object> marshalledScope = scopeRestrictions;
scopeRestrictions = new LinkedList<>();
for (Object obj : marshalledScope) {
Map<String, Object> data = (Map<String, Object>)obj;
if (data.containsKey("literals")) {
if (literalScopeRestrictions == null) {
literalScopeRestrictions = new HashSet<>();
Map<String, Object> map = new HashMap<>();
map.put("literals", literalScopeRestrictions);
scopeRestrictions.add(map);
}
Collection<String> scopes = (Collection<String>)data.get("literals");
literalScopeRestrictions.addAll(scopes);
} else if (data.containsKey("clusterRole")) {
ClusterRoleRestriction roleRestriction = new ClusterRoleRestriction();
Map<String, Object> roleData = (Map<String, Object>)data.get("clusterRole");
Collection<String> namespaces = (Collection<String>)roleData.get("namespaces");
Collection<String> roleNames = (Collection<String>)roleData.get("roleNames");
boolean allowEscalation = roleData.containsKey("allowEscalation") ? (Boolean)roleData.get("allowEscalation") : false;
roleRestriction.namespaces.addAll(namespaces);
roleRestriction.roleNames.addAll(roleNames);
roleRestriction.allowEscalation = allowEscalation;
if (clusterRoleRestrictions == null) {
clusterRoleRestrictions = new LinkedList<>();
}
clusterRoleRestrictions.add(roleRestriction);
Map<String, Object> map = new HashMap<>();
map.put("clusterRole", roleRestriction);
scopeRestrictions.add(map);
}
}
}
public void addLiteralScopeRestriction(String scope) {
if (literalScopeRestrictions == null) {
parseScopeRestrictions();
if (scopeRestrictions == null) {
scopeRestrictions = new LinkedList<>();
}
literalScopeRestrictions = new HashSet<>();
Map<String, Object> map = new HashMap<>();
map.put("literals", literalScopeRestrictions);
scopeRestrictions.add(map);
}
literalScopeRestrictions.add(scope);
}
public void addClusterRoleScopeRestriction(ClusterRoleRestriction restriction) {
parseScopeRestrictions();
if (clusterRoleRestrictions == null) {
clusterRoleRestrictions = new LinkedList<>();
}
clusterRoleRestrictions.add(restriction);
if (scopeRestrictions == null) {
scopeRestrictions = new LinkedList<>();
}
Map<String, Object> map = new HashMap<>();
map.put("clusterRole", restriction);
scopeRestrictions.add(map);
}
private static class LiteralScopeRestriction {
@JsonProperty("literals")
protected Set<String> literals = new HashSet<>();
public Set<String> getLiterals() {
return literals;
}
public void setLiterals(Set<String> literals) {
this.literals = literals;
}
}
public static class ClusterRoleRestriction {
@JsonProperty("allowEscalation")
protected boolean allowEscalation;
@JsonProperty("namespaces")
protected Set<String> namespaces = new HashSet<>();
@JsonProperty("roleNames")
protected Set<String> roleNames = new HashSet<>();
public boolean isAllowEscalation() {
return allowEscalation;
}
public void setAllowEscalation(boolean allowedEscalation) {
this.allowEscalation = allowedEscalation;
}
public Set<String> getNamespaces() {
return namespaces;
}
public void setNamespaces(Set<String> namespaces) {
this.namespaces = namespaces;
}
public Set<String> getRoleNames() {
return roleNames;
}
public void setRoleNames(Set<String> roleNames) {
this.roleNames = roleNames;
}
}
}
@GET
@Path("{name}")
@Produces(MediaType.APPLICATION_JSON)
OAuthClientRepresentation get(@PathParam("name") String name);
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
Response create(OAuthClientRepresentation client);
@DELETE
@Path("{name}")
@Produces(MediaType.APPLICATION_JSON)
Response delete(@PathParam("name") String name);
}
@@ -0,0 +1,29 @@
/*
* 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.protocol.openshift.connections.rest.apis.oauth;
import javax.ws.rs.Path;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface OpenshiftOAuth {
@Path("oauthclients")
OAuthClients clients();
}
@@ -39,3 +39,6 @@ org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticat
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
org.keycloak.protocol.docker.DockerAuthenticatorFactory
org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.BasicAuthAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.BasicAuthOTPAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.NoCookieFlowRedirectAuthenticatorFactory
@@ -17,4 +17,6 @@
org.keycloak.protocol.oidc.OIDCLoginProtocolFactory
org.keycloak.protocol.saml.SamlProtocolFactory
org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory
org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory
org.keycloak.protocol.openshift.OpenshiftLoginProtocolFactory
org.keycloak.protocol.kubernetes.KubernetesLoginProtocolFactory
@@ -0,0 +1 @@
org.keycloak.protocol.openshift.clientstorage.OpenshiftClientStorageProviderFactory
@@ -183,6 +183,18 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
addExecInfo(execs, "OTP Form", "auth-otp-form", false, 2, 1, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
expected.add(new FlowExecutions(flow, execs));
execs = new LinkedList<>();
flow = newFlow(DefaultAuthenticationFlows.OPENSHIFT_CHALLENGE_FLOW, DefaultAuthenticationFlows.OPENSHIFT_CHALLENGE_FLOW_DESCRIPTION, "basic-flow", true, true);
addExecExport(flow, null, false, "no-cookie-redirect", false, null, REQUIRED, 10);
addExecExport(flow, null, false, "basic-auth", false, null, REQUIRED, 20);
addExecExport(flow, null, false, "basic-auth-otp", false, null, DISABLED, 30);
addExecExport(flow, null, false, "auth-spnego", false, null, DISABLED, 40);
addExecInfo(execs, "Browser Redirect/Refresh", "no-cookie-redirect", false, 0, 0, REQUIRED, null, new String[]{REQUIRED});
addExecInfo(execs, "Basic Auth Challenge", "basic-auth", false, 0, 1, REQUIRED, null, new String[]{REQUIRED, DISABLED});
addExecInfo(execs, "Basic Auth Password+OTP", "basic-auth-otp", false, 0, 2, DISABLED, null, new String[]{REQUIRED, DISABLED});
addExecInfo(execs, "Kerberos", "auth-spnego", false, 0, 3, DISABLED, null, new String[]{ALTERNATIVE, REQUIRED, DISABLED});
expected.add(new FlowExecutions(flow, execs));
flow = newFlow("registration", "registration flow", "basic-flow", true, true);
addExecExport(flow, "registration form", false, "registration-page-form", true, null, REQUIRED, 10);
@@ -146,6 +146,10 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates a username and password from login form.");
addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username Form",
"Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
addProviderInfo(result, "basic-auth", "Basic Auth Challenge",
"Basic auth challenge.");
addProviderInfo(result, "basic-auth-otp", "Basic Auth Password+OTP",
"Basic auth challenge with OTP. Password param should contain a combination of password + otp. Realm's OTP policy is used to determine how to parse this. This SHOULD NOT BE USED in conjection with regular basic auth provider.");
addProviderInfo(result, "console-username-password", "Username Password Challenge",
"Proprietary challenge protocol for CLI clients that queries for username password");
addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
@@ -170,6 +174,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"User reviews and updates profile data retrieved from Identity Provider in the displayed form");
addProviderInfo(result, "idp-username-password-form", "Username Password Form for identity provider reauthentication",
"Validates a password from login form. Username is already known from identity provider authentication");
addProviderInfo(result, "no-cookie-redirect", "Browser Redirect/Refresh",
"Perform a 302 redirect to get user agent's current URI on authenticate path with an auth_session_id query parameter. This is for client's that do not support cookies.");
addProviderInfo(result, "push-button-authenticator", "TEST: Button Login",
"Just press the button to login.");
addProviderInfo(result, "reset-credential-email", "Send Reset Email", "Send email to user and wait for response.");
@@ -0,0 +1,102 @@
/*
* 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.testsuite.openshift;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.protocol.openshift.clientstorage.OpenshiftClientStorageProviderFactory;
import org.keycloak.protocol.openshift.connections.rest.OpenshiftClient;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.storage.client.ClientStorageProvider;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import javax.ws.rs.core.Response;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Properties;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractOpenshiftBaseTest extends AbstractTestRealmKeycloakTest {
/*
$ oc create sa keycloak
$ oc adm policy add-cluster-role-to-user system:auth-delegator -z keycloak
$ oc adm policy add-cluster-role-to-user system:master -z keycloak
$ oc describe sa keycloak
# look for token and describe it
$ oc describe secret xxxxx-xxx
# copy token to MASTER_TOKEN constant above
*/
protected String addComponent(ComponentRepresentation component) {
Response resp = adminClient.realm("test").components().add(component);
resp.close();
String id = ApiUtil.getCreatedId(resp);
getCleanup().addComponentId(id);
return id;
}
@Before
public void addProvidersBeforeTest() throws URISyntaxException, IOException {
List<ComponentRepresentation> reps = adminClient.realm("test").components().query(null, ClientStorageProvider.class.getName());
if (reps.size() > 0) return;
ComponentRepresentation provider = new ComponentRepresentation();
provider.setName("openshift oauth client provider");
provider.setProviderId(OpenshiftClientStorageProviderFactory.PROVIDER_ID);
provider.setProviderType(ClientStorageProvider.class.getName());
provider.setConfig(new MultivaluedHashMap<>());
provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.ACCESS_TOKEN, getMasterToken());
provider.getConfig().putSingle(OpenshiftClientStorageProviderFactory.OPENSHIFT_URI, getOpenshiftUrl());
addComponent(provider);
}
public static Properties config = new Properties();
public static final String OPENSHIFT_CONFIG = "openshift.config";
@BeforeClass
public static void loadConfig() throws Exception {
Assume.assumeTrue(System.getProperties().containsKey(OPENSHIFT_CONFIG));
config.load(new FileInputStream(System.getProperty(OPENSHIFT_CONFIG)));
//config.put("master_token", "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJvcGVuc2hpZnQtd2ViLWNvbnNvbGUiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoia2V5Y2xvYWstdG9rZW4tcHFmcWQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoia2V5Y2xvYWsiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiI4MmU5ZTE0YS0zYWRjLTExZTgtYmZkMy0wMDBjMjk4MmM5YjgiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6b3BlbnNoaWZ0LXdlYi1jb25zb2xlOmtleWNsb2FrIn0.MP84QI0HVg6Xnk1sZhmzcXbDoN0mAdFZm061_tdyjBlqR34oKKHeZ_4q_kja7xZnmg3t0a7ZZ2mW0mafVVXwjiDucdYMkdJ5vEmi291kNz3Qut9hS8dqWBY0tt4hnwzjDr_4O6mfO66CFdjLs_9uJfqXFQpH8xi4RI98fPAqMAiqQ69tt9SVkt7ozTJI-qCSADjFlZeWnWSGP8ruX5lI9URCSlpfSiDdZeXzU6WQUh7pdjbq2mp64azfg8Nv5RfQQ5w9ZFMEhFqoJNelO5UVIN6JdpjrpLbsm5TGDIg68_RhCcdWY_lGDjE-gE2_qxulIfbKHyruKxU5uolgLaCIAw");
//config.put("master_token", "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJteXByb2plY3QiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoia2V5Y2xvYWstdG9rZW4tY2M4cWwiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoia2V5Y2xvYWsiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzMzg0NTU3OS0zYTg5LTExZTgtOGQ2Mi0wMDBjMjk4MmM5YjgiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6bXlwcm9qZWN0OmtleWNsb2FrIn0.KDsjSh5Mai43Q9oj9Zeokz6AphGHEV9TdhTK9y7qj-F976gUhQw7dmNOAs22m7CPjbgpZHlom6KZD0-yUP4_ZgE6r6kKw_lN_8YvM_4sMG_bZNrl9y5nk5tIrRsIexQ-inXo9AxnlMPTGClZTfePMOmoP6Ubz9OMvxZLL066kKNVRhoE0yE-Nv1o1GHnnYmx9q-LEnL5IxSmI1_fT28bceDgisxhopVAoQoFX7T5TmvBnMoZwJ6ba3ZQm7z0ISIf-ur1W77-NbYLOx9lepiT4si5OSO9qdzfUQjBc6YL8-ueU_QrmrftJYwc9tvvLOo2b0jndd6N1ApNnyx1niSdaA");
//config.put("openshift_url", "https://192.168.238.132:8443");
}
public static String getMasterToken() {
return config.getProperty("master_token");
}
public static String getOpenshiftUrl() {
return config.getProperty("openshift_url");
}
public static OpenshiftClient createOpenshiftClient() {
return OpenshiftClient.instance(AbstractOpenshiftBaseTest.getOpenshiftUrl(), AbstractOpenshiftBaseTest.getMasterToken());
}
}
@@ -0,0 +1,297 @@
/*
* 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.testsuite.openshift;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.models.*;
import org.keycloak.protocol.openshift.OpenshiftProtocolEndpoint;
import org.keycloak.protocol.openshift.connections.rest.OpenshiftClient;
import org.keycloak.protocol.openshift.connections.rest.api.v1.Secrets;
import org.keycloak.protocol.openshift.connections.rest.api.v1.ServiceAccounts;
import org.keycloak.protocol.openshift.connections.rest.apis.oauth.OAuthClients;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.util.BasicAuthHelper;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
//@Ignore
public class CodeToTokenTest extends AbstractOpenshiftBaseTest {
public static final String OPENSHIFT_OAUTH_CLIENT = "literal-oauth-client";
public static final String SERVICE_ACCOUNT = "system:serviceaccount:myproject:sa-oauth";
public static final String SERVICE_ACCOUNT_NAME = "sa-oauth";
public static final String OPENSHIFT_OAUTH_CLIENT_WITH_CHALLENGE = "challenge-oauth-client";
public static final String SERVICE_ACCOUNT_WITH_CHALLENGE = "system:serviceaccount:myproject:sa-challenge";
public static final String SERVICE_ACCOUNT_WITH_CHALLENGE_NAME = "sa-challenge";
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Deployment
public static WebArchive deploy() {
return RunOnServerDeployment.create(UserResource.class)
.addPackages(true, "org.keycloak.testsuite");
}
@Before
public void setupTest() {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
if (session.users().getUserByUsername("reviewer", realm) != null) return;
ClientModel client = session.realms().getClientByClientId("test-app", realm);
client.setDirectAccessGrantsEnabled(true);
GroupModel group = realm.createGroup("openshift");
GroupModel child = realm.createGroup("child");
realm.moveGroup(child, group);
UserModel user = session.users().addUser(realm, "reviewer");
user.setEnabled(true);
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
user.joinGroup(child);
});
}
protected String sa_token;
protected String sa_challenge_token;
@Before
public void createClients() throws Exception {
if (sa_token != null) return;
OpenshiftClient client = AbstractOpenshiftBaseTest.createOpenshiftClient();
OAuthClients.OAuthClientRepresentation rep = OAuthClients.OAuthClientRepresentation.create();
// with literal scope restriction
client.apis().oauth().clients().delete(OPENSHIFT_OAUTH_CLIENT).close();
rep.setName(OPENSHIFT_OAUTH_CLIENT);
rep.setGrantMethod("auto");
rep.setSecret("geheim");
rep.setRespondWithChallenges(false);
rep.addRedirectURI("http://host1");
rep.addRedirectURI("http://host2");
rep.addLiteralScopeRestriction("foo");
rep.addLiteralScopeRestriction("foo:bar");
OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction restriction = new OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction();
restriction.setAllowEscalation(true);
restriction.getNamespaces().add("myproject");
restriction.getRoleNames().add("myrole");
rep.addClusterRoleScopeRestriction(restriction);
restriction = new OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction();
restriction.setAllowEscalation(false);
restriction.getNamespaces().add("myproject");
restriction.getRoleNames().add("another");
rep.addClusterRoleScopeRestriction(restriction);
client.apis().oauth().clients().create(rep).close();
rep = OAuthClients.OAuthClientRepresentation.create();
// with literal scope restriction
client.apis().oauth().clients().delete(OPENSHIFT_OAUTH_CLIENT_WITH_CHALLENGE).close();
rep.setName(OPENSHIFT_OAUTH_CLIENT_WITH_CHALLENGE);
rep.setGrantMethod("auto");
rep.setSecret("geheim");
rep.setRespondWithChallenges(true);
rep.addRedirectURI("http://localhost");
client.apis().oauth().clients().create(rep).close();
client.api().namespace("myproject").serviceAccounts().delete(SERVICE_ACCOUNT_NAME).close();
ServiceAccounts.ServiceAccountRepresentation sa = new ServiceAccounts.ServiceAccountRepresentation();
sa.setName(SERVICE_ACCOUNT_NAME);
sa.setNamespace("myproject");
sa.setOauthWantChallenges(false);
sa.addRedirectUri("http:/host1");
sa = client.api().namespace("myproject").serviceAccounts().create(sa);
for (int i = 0; i < 5; i++) {
// sleep as generating secrets takes awhile.
Thread.sleep(1000);
sa = client.api().namespace("myproject").serviceAccounts().get(SERVICE_ACCOUNT_NAME);
if (sa.getSecrets().isEmpty()) continue;
break;
}
for (String secret : sa.getSecrets()) {
Secrets.SecretRepresentation secretRep = client.api().namespace("myproject").secrets().get(secret);
if (secretRep.isServiceAccountToken()) {
sa_token = secretRep.getToken();
//System.out.println(client.api().namespace("myproject").secrets().getPretty(secret, true));
break;
}
}
Assert.assertNotNull(sa_token);
client.api().namespace("myproject").serviceAccounts().delete(SERVICE_ACCOUNT_WITH_CHALLENGE_NAME).close();
sa = new ServiceAccounts.ServiceAccountRepresentation();
sa.setName(SERVICE_ACCOUNT_WITH_CHALLENGE_NAME);
sa.setNamespace("myproject");
sa.setOauthWantChallenges(true);
sa.addRedirectUri("http://localhost");
sa = client.api().namespace("myproject").serviceAccounts().create(sa);
for (int i = 0; i < 5; i++) {
// sleep as generating secrets takes awhile.
Thread.sleep(1000);
sa = client.api().namespace("myproject").serviceAccounts().get(SERVICE_ACCOUNT_WITH_CHALLENGE_NAME);
if (sa.getSecrets().isEmpty()) continue;
break;
}
for (String secret : sa.getSecrets()) {
Secrets.SecretRepresentation secretRep = client.api().namespace("myproject").secrets().get(secret);
if (secretRep.isServiceAccountToken()) {
sa_challenge_token = secretRep.getToken();
//System.out.println(client.api().namespace("myproject").secrets().getPretty(secret, true));
break;
}
}
Assert.assertNotNull(sa_challenge_token);
client.close();
}
@Test
public void testOAuthClient() throws Exception {
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
String grantUri = getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
String tokenReviewUrl = getTokenReviewUrl();
WebTarget tokenReviewTarget = httpClient.target(tokenReviewUrl);
oauth.clientId(OPENSHIFT_OAUTH_CLIENT_WITH_CHALLENGE);
oauth.redirectUri("http://localhost");
WebTarget authUrl = httpClient.target(oauth.getLoginFormUrl());
Response response = authUrl.request().get();
Assert.assertEquals(302, response.getStatus());
response.close();
URI location = response.getLocation();
response = httpClient.target(location).request().get();
Assert.assertEquals(401, response.getStatus());
Assert.assertNotNull(response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE));
response.close();
String header = BasicAuthHelper.createHeader("reviewer", "password");
response = httpClient.target(location).request().header(HttpHeaders.AUTHORIZATION, header).get();
Assert.assertEquals(302, response.getStatus());
location = response.getLocation();
response.close();
Pattern codePattern = Pattern.compile("code=([^&]+)");
String redirect = response.getLocation().toString();
Matcher m = codePattern.matcher(redirect);
Assert.assertTrue(m.find());
String code = m.group(1);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "geheim");
Assert.assertNotNull(tokenResponse.getAccessToken());
httpClient.close();
events.clear();
}
@Test
public void testServiceAccount() throws Exception {
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
String grantUri = getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
String tokenReviewUrl = getTokenReviewUrl();
WebTarget tokenReviewTarget = httpClient.target(tokenReviewUrl);
oauth.clientId(SERVICE_ACCOUNT_WITH_CHALLENGE);
oauth.redirectUri("http://localhost");
WebTarget authUrl = httpClient.target(oauth.getLoginFormUrl());
Response response = authUrl.request().get();
Assert.assertEquals(302, response.getStatus());
response.close();
URI location = response.getLocation();
response = httpClient.target(location).request().get();
Assert.assertEquals(401, response.getStatus());
Assert.assertNotNull(response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE));
response.close();
String header = BasicAuthHelper.createHeader("reviewer", "password");
response = httpClient.target(location).request().header(HttpHeaders.AUTHORIZATION, header).get();
Assert.assertEquals(302, response.getStatus());
location = response.getLocation();
response.close();
Pattern codePattern = Pattern.compile("code=([^&]+)");
String redirect = response.getLocation().toString();
Matcher m = codePattern.matcher(redirect);
Assert.assertTrue(m.find());
String code = m.group(1);
String accessTokenUrl = oauth.getAccessTokenUrl();
Form form = new Form();
form.param("client_id", SERVICE_ACCOUNT_WITH_CHALLENGE);
form.param("client_secret", sa_challenge_token);
form.param(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri());
form.param(OAuth2Constants.CODE, code);
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE);
response = httpClient.target(accessTokenUrl).request().post(Entity.form(form));
Assert.assertEquals(200, response.getStatus());
//String data = response.readEntity(String.class);
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
//OAuthClient.AccessTokenResponse tokenResponse =response.readEntity(OAuthClient.AccessTokenResponse.class);
Assert.assertNotNull(tokenResponse.getToken());
httpClient.close();
events.clear();
}
public String getResourceOwnerPasswordCredentialGrantUrl() {
UriBuilder b = OpenshiftProtocolEndpoint.tokenUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
return b.build("test").toString();
}
public String getTokenReviewUrl() {
UriBuilder b = OpenshiftProtocolEndpoint.tokenReviewnUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
return b.build("test").toString();
}
}
@@ -0,0 +1,208 @@
/*
* 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.testsuite.openshift;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.openshift.OpenshiftProtocolEndpoint;
import org.keycloak.protocol.openshift.TokenReviewRequestRepresentation;
import org.keycloak.protocol.openshift.connections.rest.OpenshiftClient;
import org.keycloak.protocol.openshift.connections.rest.api.v1.Secrets;
import org.keycloak.protocol.openshift.connections.rest.api.v1.ServiceAccounts;
import org.keycloak.protocol.openshift.connections.rest.apis.oauth.OAuthClients;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.util.BasicAuthHelper;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
//@Ignore
public class OpenshiftClientStorageTest extends AbstractOpenshiftBaseTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Deployment
public static WebArchive deploy() {
return RunOnServerDeployment.create(UserResource.class)
.addPackages(true, "org.keycloak.testsuite");
}
protected String sa_token;
@Before
public void createClients() throws Exception {
if (sa_token != null) return;
OpenshiftClient client = AbstractOpenshiftBaseTest.createOpenshiftClient();
OAuthClients.OAuthClientRepresentation rep = OAuthClients.OAuthClientRepresentation.create();
// with literal scope restriction
client.apis().oauth().clients().delete("literal-oauth-client").close();
rep.setName("literal-oauth-client");
rep.setGrantMethod("auto");
rep.setSecret("geheim");
rep.setRespondWithChallenges(false);
rep.addRedirectURI("http://host1");
rep.addRedirectURI("http://host2");
rep.addLiteralScopeRestriction("foo");
rep.addLiteralScopeRestriction("foo:bar");
client.apis().oauth().clients().create(rep).close();
client.api().namespace("myproject").serviceAccounts().delete("sa-oauth").close();
ServiceAccounts.ServiceAccountRepresentation sa = new ServiceAccounts.ServiceAccountRepresentation();
sa.setName("sa-oauth");
sa.setNamespace("myproject");
sa.setOauthWantChallenges(true);
sa.addRedirectUri("http:/host1");
sa.addRedirectUri("http:/host2");
sa = client.api().namespace("myproject").serviceAccounts().create(sa);
for (int i = 0; i < 5; i++) {
// sleep as generating secrets takes awhile.
Thread.sleep(1000);
sa = client.api().namespace("myproject").serviceAccounts().get("sa-oauth");
if (sa.getSecrets().isEmpty()) continue;
break;
}
for (String secret : sa.getSecrets()) {
Secrets.SecretRepresentation secretRep = client.api().namespace("myproject").secrets().get(secret);
if (secretRep.isServiceAccountToken()) {
sa_token = secretRep.getToken();
//System.out.println(client.api().namespace("myproject").secrets().getPretty(secret, true));
break;
}
}
Assert.assertNotNull(sa_token);
TokenReviewRequestRepresentation request = TokenReviewRequestRepresentation.create(sa_token);
String response = client.apis().kubernetesAuthentication().tokenReview().reviewPretty(request, true);
//System.out.println(response);
client.close();
}
//@Test
public void testServer() throws Exception {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
ClientModel client = session.realms().getClientByClientId("kcinit", realm);
if (client != null) {
return;
}
ClientModel kcinit = realm.addClient("kcinit");
kcinit.setEnabled(true);
kcinit.addRedirectUri("http://localhost:*");
kcinit.setPublicClient(true);
});
Thread.sleep(10000000000000l);
}
@Test
public void testOAuthClient() throws Exception {
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
String grantUri = getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
String accessToken = null;
{ // test valid password
String header = BasicAuthHelper.createHeader("literal-oauth-client", "geheim");
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("username", "test-user@localhost");
form.param("password", "password");
form.param("scope", "oauth openid");
Response response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, header)
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
}
httpClient.close();
events.clear();
}
@Test
public void testServiceAccount() throws Exception {
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
String grantUri = getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
String accessToken = null;
{ // test valid password
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("client_id", "system:serviceaccount:myproject:sa-oauth");
form.param("client_secret", sa_token);
form.param("username", "test-user@localhost");
form.param("password", "password");
form.param("scope", "oauth openid");
Response response = grantTarget.request()
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
}
httpClient.close();
events.clear();
}
public String getResourceOwnerPasswordCredentialGrantUrl() {
UriBuilder b = OpenshiftProtocolEndpoint.tokenUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
return b.build("test").toString();
}
}
@@ -0,0 +1,213 @@
/*
* 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.testsuite.openshift;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.protocol.openshift.TokenReviewRequestRepresentation;
import org.keycloak.protocol.openshift.connections.rest.OpenshiftClient;
import org.keycloak.protocol.openshift.connections.rest.api.v1.Namespace;
import org.keycloak.protocol.openshift.connections.rest.api.v1.Secrets;
import org.keycloak.protocol.openshift.connections.rest.api.v1.ServiceAccounts;
import org.keycloak.protocol.openshift.connections.rest.apis.oauth.OAuthClients;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import java.io.FileInputStream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
//@Ignore
public class OpenshiftClientTest {
@BeforeClass
public static void loadConfig() throws Exception {
Assume.assumeTrue(System.getProperties().containsKey(AbstractOpenshiftBaseTest.OPENSHIFT_CONFIG));
AbstractOpenshiftBaseTest.config.load(new FileInputStream(System.getProperty(AbstractOpenshiftBaseTest.OPENSHIFT_CONFIG)));
}
@Test
public void testServiceAccount() throws Exception {
OpenshiftClient client = AbstractOpenshiftBaseTest.createOpenshiftClient();
Namespace myproject = client.api().namespace("myproject");
// test not found
try {
myproject.serviceAccounts().get("notfound");
Assert.fail();
} catch (NotFoundException e) {
if (e.getResponse() != null) e.getResponse().close();
}
Response response = myproject.serviceAccounts().delete("sa-oauth");
response.close();
ServiceAccounts.ServiceAccountRepresentation rep = new ServiceAccounts.ServiceAccountRepresentation();
rep.setName("sa-oauth");
rep.setNamespace("myproject");
rep.setOauthWantChallenges(true);
rep.addRedirectUri("http:/host1");
rep.addRedirectUri("http:/host2");
rep = myproject.serviceAccounts().create(rep);
for (int i = 0; i < 5; i++) {
// sleep as generating secrets takes awhile.
Thread.sleep(1000);
rep = myproject.serviceAccounts().get("sa-oauth");
if (rep.getSecrets().isEmpty()) continue;
break;
}
Assert.assertEquals("sa-oauth", rep.getName());
Assert.assertEquals("myproject", rep.getNamespace());
Assert.assertTrue(rep.getOauthRedirectUris().contains("http:/host1"));
Assert.assertTrue(rep.getOauthRedirectUris().contains("http:/host2"));
Assert.assertTrue(rep.oauthWantChallenges());
String token = null;
for (String secret : rep.getSecrets()) {
Secrets.SecretRepresentation secretRep = myproject.secrets().get(secret);
if (secretRep.isServiceAccountToken()) {
token = secretRep.getToken();
}
}
Assert.assertNotNull(token);
response = myproject.serviceAccounts().delete("sa-oauth");
Assert.assertEquals(200, response.getStatus());
response.close();
try {
rep = myproject.serviceAccounts().get("sa-oauth");
Assert.fail("should be deleted already");
} catch (NotFoundException e) {
}
}
@Test
public void testTokenReview() throws Exception {
OpenshiftClient client = AbstractOpenshiftBaseTest.createOpenshiftClient();
TokenReviewRequestRepresentation request = TokenReviewRequestRepresentation.create(AbstractOpenshiftBaseTest.getMasterToken());
//TokenReviewResponseRepresentation review = client.apis().kubernetesAuthentication("v1beta1").tokenReview().review(request);
Response response = client.apis().kubernetesAuthentication().tokenReview().review(request);
String data = response.readEntity(String.class);
//System.out.println(data);
}
@Test
public void testOAuthClients() throws Exception {
OpenshiftClient client = AbstractOpenshiftBaseTest.createOpenshiftClient();
OAuthClients.OAuthClientRepresentation rep = OAuthClients.OAuthClientRepresentation.create();
// with literal scope restriction
client.apis().oauth().clients().delete("literal-oauth-client").close();
rep.setName("literal-oauth-client");
rep.setGrantMethod("auto");
rep.setSecret("geheim");
rep.setRespondWithChallenges(false);
rep.addRedirectURI("http://host1");
rep.addRedirectURI("http://host2");
rep.addLiteralScopeRestriction("foo");
rep.addLiteralScopeRestriction("foo:bar");
client.apis().oauth().clients().create(rep).close();
rep = client.apis().oauth().clients().get("literal-oauth-client");
Assert.assertEquals("literal-oauth-client", rep.getName());
Assert.assertEquals("auto", rep.getGrantMethod());
Assert.assertEquals("geheim", rep.getSecret());
Assert.assertFalse(rep.isRespondWithChallenges());
Assert.assertTrue(rep.getRedirectURIs().contains("http://host1"));
Assert.assertTrue(rep.getRedirectURIs().contains("http://host2"));
Assert.assertTrue(rep.getLiteralScopeRestrictions().contains("foo"));
Assert.assertTrue(rep.getLiteralScopeRestrictions().contains("foo:bar"));
client.apis().oauth().clients().delete("literal-oauth-client").close();
// with cluster role scope restriction
client.apis().oauth().clients().delete("role-oauth-client").close();
rep = OAuthClients.OAuthClientRepresentation.create();
rep.setName("role-oauth-client");
rep.setGrantMethod("prompt");
rep.setSecret("geheim");
rep.setRespondWithChallenges(true);
rep.addRedirectURI("http://host1");
OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction restriction = new OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction();
restriction.setAllowEscalation(true);
restriction.getNamespaces().add("foo");
restriction.getRoleNames().add("bar");
rep.addClusterRoleScopeRestriction(restriction);
client.apis().oauth().clients().create(rep).close();
rep = client.apis().oauth().clients().get("role-oauth-client");
Assert.assertEquals("role-oauth-client", rep.getName());
Assert.assertEquals("prompt", rep.getGrantMethod());
Assert.assertEquals("geheim", rep.getSecret());
Assert.assertTrue(rep.isRespondWithChallenges());
Assert.assertTrue(rep.getRedirectURIs().contains("http://host1"));
Assert.assertEquals(1, rep.getClusterRoleRestrictions().size());
OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction testRestriction = rep.getClusterRoleRestrictions().get(0);
Assert.assertTrue(testRestriction.isAllowEscalation());
Assert.assertTrue(testRestriction.getNamespaces().contains("foo"));
Assert.assertTrue(testRestriction.getRoleNames().contains("bar"));
client.apis().oauth().clients().delete("role-oauth-client").close();
// with both literal and cluster role scope restriction
client.apis().oauth().clients().delete("both-oauth-client").close();
rep = OAuthClients.OAuthClientRepresentation.create();
rep.setName("both-oauth-client");
rep.setGrantMethod("prompt");
rep.setSecret("geheim");
rep.setRespondWithChallenges(true);
rep.addRedirectURI("http://host1");
rep.addClusterRoleScopeRestriction(restriction);
rep.addLiteralScopeRestriction("foo");
rep.addLiteralScopeRestriction("foo:bar");
client.apis().oauth().clients().create(rep).close();
rep = client.apis().oauth().clients().get("both-oauth-client");
Assert.assertEquals("both-oauth-client", rep.getName());
Assert.assertEquals("prompt", rep.getGrantMethod());
Assert.assertEquals("geheim", rep.getSecret());
Assert.assertTrue(rep.isRespondWithChallenges());
Assert.assertTrue(rep.getRedirectURIs().contains("http://host1"));
Assert.assertEquals(1, rep.getClusterRoleRestrictions().size());
testRestriction = rep.getClusterRoleRestrictions().get(0);
Assert.assertTrue(testRestriction.isAllowEscalation());
Assert.assertTrue(testRestriction.getNamespaces().contains("foo"));
Assert.assertTrue(testRestriction.getRoleNames().contains("bar"));
Assert.assertTrue(rep.getLiteralScopeRestrictions().contains("foo"));
Assert.assertTrue(rep.getLiteralScopeRestrictions().contains("foo:bar"));
client.apis().oauth().clients().delete("both-oauth-client").close();
}
}
@@ -0,0 +1,439 @@
/*
* 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.testsuite.openshift;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.Errors;
import org.keycloak.models.*;
import org.keycloak.protocol.openshift.OpenshiftProtocolEndpoint;
import org.keycloak.protocol.openshift.TokenReviewRequestRepresentation;
import org.keycloak.protocol.openshift.TokenReviewResponseRepresentation;
import org.keycloak.protocol.openshift.connections.rest.OpenshiftClient;
import org.keycloak.protocol.openshift.connections.rest.api.v1.Secrets;
import org.keycloak.protocol.openshift.connections.rest.api.v1.ServiceAccounts;
import org.keycloak.protocol.openshift.connections.rest.apis.oauth.OAuthClients;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.util.List;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
//@Ignore
public class TokenReviewEndpointTest extends AbstractOpenshiftBaseTest {
public static final String OPENSHIFT_OAUTH_CLIENT = "literal-oauth-client";
public static final String SERVICE_ACCOUNT = "system:serviceaccount:myproject:sa-oauth";
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Deployment
public static WebArchive deploy() {
return RunOnServerDeployment.create(UserResource.class)
.addPackages(true, "org.keycloak.testsuite");
}
@Before
public void setupTest() {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
if (session.users().getUserByUsername("reviewer", realm) != null) return;
ClientModel client = session.realms().getClientByClientId("test-app", realm);
client.setDirectAccessGrantsEnabled(true);
GroupModel group = realm.createGroup("openshift");
GroupModel child = realm.createGroup("child");
realm.moveGroup(child, group);
UserModel user = session.users().addUser(realm, "reviewer");
user.setEnabled(true);
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
user.joinGroup(child);
});
}
protected String sa_token;
@Before
public void createClients() throws Exception {
if (sa_token != null) return;
OpenshiftClient client = createOpenshiftClient();
OAuthClients.OAuthClientRepresentation rep = OAuthClients.OAuthClientRepresentation.create();
// with literal scope restriction
client.apis().oauth().clients().delete(OPENSHIFT_OAUTH_CLIENT).close();
rep.setName(OPENSHIFT_OAUTH_CLIENT);
rep.setGrantMethod("auto");
rep.setSecret("geheim");
rep.setRespondWithChallenges(false);
rep.addRedirectURI("http://host1");
rep.addRedirectURI("http://host2");
rep.addLiteralScopeRestriction("foo");
rep.addLiteralScopeRestriction("foo:bar");
OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction restriction = new OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction();
restriction.setAllowEscalation(true);
restriction.getNamespaces().add("myproject");
restriction.getRoleNames().add("myrole");
rep.addClusterRoleScopeRestriction(restriction);
restriction = new OAuthClients.OAuthClientRepresentation.ClusterRoleRestriction();
restriction.setAllowEscalation(false);
restriction.getNamespaces().add("myproject");
restriction.getRoleNames().add("another");
rep.addClusterRoleScopeRestriction(restriction);
client.apis().oauth().clients().create(rep).close();
client.api().namespace("myproject").serviceAccounts().delete("sa-oauth").close();
ServiceAccounts.ServiceAccountRepresentation sa = new ServiceAccounts.ServiceAccountRepresentation();
sa.setName("sa-oauth");
sa.setNamespace("myproject");
sa.setOauthWantChallenges(true);
sa.addRedirectUri("http:/host1");
sa.addRedirectUri("http:/host2");
sa = client.api().namespace("myproject").serviceAccounts().create(sa);
for (int i = 0; i < 5; i++) {
// sleep as generating secrets takes awhile.
Thread.sleep(1000);
sa = client.api().namespace("myproject").serviceAccounts().get("sa-oauth");
if (sa.getSecrets().isEmpty()) continue;
break;
}
for (String secret : sa.getSecrets()) {
Secrets.SecretRepresentation secretRep = client.api().namespace("myproject").secrets().get(secret);
if (secretRep.isServiceAccountToken()) {
sa_token = secretRep.getToken();
//System.out.println(client.api().namespace("myproject").secrets().getPretty(secret, true));
break;
}
}
Assert.assertNotNull(sa_token);
client.close();
}
@Test
public void testOAuthClient() throws Exception {
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
String grantUri = getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
String tokenReviewUrl = getTokenReviewUrl();
WebTarget tokenReviewTarget = httpClient.target(tokenReviewUrl);
// test good scope/token
{
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("client_id", OPENSHIFT_OAUTH_CLIENT);
form.param("client_secret", "geheim");
form.param("username", "reviewer");
form.param("password", "password");
form.param("scope", "foo foo:bar role:myrole:myproject");
Response response = grantTarget.request()
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
response = tokenReviewTarget.request()
.post(Entity.json(TokenReviewRequestRepresentation.build(accessToken)));
assertEquals(200, response.getStatus());
String reviewString = response.readEntity(String.class);
//System.out.println(reviewString);
TokenReviewResponseRepresentation review = JsonSerialization.readValue(reviewString, TokenReviewResponseRepresentation.class);
Assert.assertTrue(review.getStatus().isAuthenticated());
TokenReviewResponseRepresentation.Status.User user = review.getStatus().getUser();
Assert.assertEquals("reviewer", user.getUsername());
Assert.assertTrue(user.getGroups().contains("openshift"));
Assert.assertTrue(user.getGroups().contains("openshift:child"));
List<String> scopes = (List<String>)user.getExtra().getData().get("scopes.authorization.openshift.io");
Assert.assertTrue(scopes.contains("foo"));
Assert.assertTrue(scopes.contains("foo:bar"));
Assert.assertTrue(scopes.contains("role:myrole:myproject"));
}
// test escalation
{
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("client_id", OPENSHIFT_OAUTH_CLIENT);
form.param("client_secret", "geheim");
form.param("username", "reviewer");
form.param("password", "password");
form.param("scope", "role:myrole:myproject:! role:another:myproject");
Response response = grantTarget.request()
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
response = tokenReviewTarget.request()
.post(Entity.json(TokenReviewRequestRepresentation.build(accessToken)));
assertEquals(200, response.getStatus());
String reviewString = response.readEntity(String.class);
//System.out.println(reviewString);
TokenReviewResponseRepresentation review = JsonSerialization.readValue(reviewString, TokenReviewResponseRepresentation.class);
Assert.assertTrue(review.getStatus().isAuthenticated());
TokenReviewResponseRepresentation.Status.User user = review.getStatus().getUser();
Assert.assertEquals("reviewer", user.getUsername());
List<String> scopes = (List<String>)user.getExtra().getData().get("scopes.authorization.openshift.io");
Assert.assertTrue(scopes.contains("role:myrole:myproject:!"));
Assert.assertTrue(scopes.contains("role:another:myproject"));
}
// test bad scope
{
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("client_id", OPENSHIFT_OAUTH_CLIENT);
form.param("client_secret", "geheim");
form.param("username", "reviewer");
form.param("password", "password");
form.param("scope", "bad_scope");
Response response = grantTarget.request()
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
response = tokenReviewTarget.request()
.post(Entity.json(TokenReviewRequestRepresentation.build(accessToken)));
assertEquals(401, response.getStatus());
String reviewString = response.readEntity(String.class);
//System.out.println(reviewString);
TokenReviewResponseRepresentation review = JsonSerialization.readValue(reviewString, TokenReviewResponseRepresentation.class);
Assert.assertFalse(review.getStatus().isAuthenticated());
Assert.assertEquals(Errors.INVALID_SCOPE, review.getStatus().getError());
}
// test bad escalation
{
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("client_id", OPENSHIFT_OAUTH_CLIENT);
form.param("client_secret", "geheim");
form.param("username", "reviewer");
form.param("password", "password");
form.param("scope", "role:another:myproject:!");
Response response = grantTarget.request()
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
response = tokenReviewTarget.request()
.post(Entity.json(TokenReviewRequestRepresentation.build(accessToken)));
assertEquals(401, response.getStatus());
String reviewString = response.readEntity(String.class);
//System.out.println(reviewString);
TokenReviewResponseRepresentation review = JsonSerialization.readValue(reviewString, TokenReviewResponseRepresentation.class);
Assert.assertFalse(review.getStatus().isAuthenticated());
Assert.assertEquals(Errors.INVALID_SCOPE, review.getStatus().getError());
}
httpClient.close();
events.clear();
}
@Test
public void testServiceAccount() throws Exception {
Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
String grantUri = getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
String tokenReviewUrl = getTokenReviewUrl();
WebTarget tokenReviewTarget = httpClient.target(tokenReviewUrl);
// test good scope/token
{
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("client_id", SERVICE_ACCOUNT);
form.param("client_secret", sa_token);
form.param("username", "reviewer");
form.param("password", "password");
form.param("scope", "user:info user:check-access role:myrole:myproject role:another:myproject:!");
Response response = grantTarget.request()
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
response = tokenReviewTarget.request()
.post(Entity.json(TokenReviewRequestRepresentation.build(accessToken)));
assertEquals(200, response.getStatus());
String reviewString = response.readEntity(String.class);
//System.out.println(reviewString);
TokenReviewResponseRepresentation review = JsonSerialization.readValue(reviewString, TokenReviewResponseRepresentation.class);
Assert.assertTrue(review.getStatus().isAuthenticated());
TokenReviewResponseRepresentation.Status.User user = review.getStatus().getUser();
Assert.assertEquals("reviewer", user.getUsername());
Assert.assertTrue(user.getGroups().contains("openshift"));
Assert.assertTrue(user.getGroups().contains("openshift:child"));
List<String> scopes = (List<String>)user.getExtra().getData().get("scopes.authorization.openshift.io");
Assert.assertTrue(scopes.contains("user:info"));
Assert.assertTrue(scopes.contains("user:check-access"));
Assert.assertTrue(scopes.contains("role:myrole:myproject"));
Assert.assertTrue(scopes.contains("role:another:myproject:!"));
}
// test bad scope
{
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("client_id", SERVICE_ACCOUNT);
form.param("client_secret", sa_token);
form.param("username", "reviewer");
form.param("password", "password");
form.param("scope", "bad_scope");
Response response = grantTarget.request()
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
response = tokenReviewTarget.request()
.post(Entity.json(TokenReviewRequestRepresentation.build(accessToken)));
assertEquals(401, response.getStatus());
String reviewString = response.readEntity(String.class);
//System.out.println(reviewString);
TokenReviewResponseRepresentation review = JsonSerialization.readValue(reviewString, TokenReviewResponseRepresentation.class);
Assert.assertFalse(review.getStatus().isAuthenticated());
Assert.assertEquals(Errors.INVALID_SCOPE, review.getStatus().getError());
}
// test bad role namespace
{
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("client_id", SERVICE_ACCOUNT);
form.param("client_secret", sa_token);
form.param("username", "reviewer");
form.param("password", "password");
form.param("scope", "role:another:badproject");
Response response = grantTarget.request()
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
response = tokenReviewTarget.request()
.post(Entity.json(TokenReviewRequestRepresentation.build(accessToken)));
assertEquals(401, response.getStatus());
String reviewString = response.readEntity(String.class);
//System.out.println(reviewString);
TokenReviewResponseRepresentation review = JsonSerialization.readValue(reviewString, TokenReviewResponseRepresentation.class);
Assert.assertFalse(review.getStatus().isAuthenticated());
Assert.assertEquals(Errors.INVALID_SCOPE, review.getStatus().getError());
}
// test bad role namespace
{
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param("client_id", SERVICE_ACCOUNT);
form.param("client_secret", sa_token);
form.param("username", "reviewer");
form.param("password", "password");
form.param("scope", "role:another:badproject:!");
Response response = grantTarget.request()
.post(Entity.form(form));
assertEquals(200, response.getStatus());
AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
String accessToken = tokenResponse.getToken();
Assert.assertNotNull(accessToken);
response.close();
response = tokenReviewTarget.request()
.post(Entity.json(TokenReviewRequestRepresentation.build(accessToken)));
assertEquals(401, response.getStatus());
String reviewString = response.readEntity(String.class);
//System.out.println(reviewString);
TokenReviewResponseRepresentation review = JsonSerialization.readValue(reviewString, TokenReviewResponseRepresentation.class);
Assert.assertFalse(review.getStatus().isAuthenticated());
Assert.assertEquals(Errors.INVALID_SCOPE, review.getStatus().getError());
}
httpClient.close();
events.clear();
}
public String getResourceOwnerPasswordCredentialGrantUrl() {
UriBuilder b = OpenshiftProtocolEndpoint.tokenUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
return b.build("test").toString();
}
public String getTokenReviewUrl() {
UriBuilder b = OpenshiftProtocolEndpoint.tokenReviewnUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
return b.build("test").toString();
}
}