From 7f7e0cce1b38b199d9a8c22a5e85e18e5c37c7c5 Mon Sep 17 00:00:00 2001
From: Matthias Piepkorn <mpiepk@gmail.com>
Date: Fri, 27 Jan 2017 22:45:31 +0000
Subject: [PATCH] Add protocol SPIs, endpoints for login/logout/serviceValidate and some claim mapper stubs
---
src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java | 70 +++
src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java | 92 ++++
src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java | 50 ++
src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory | 18
src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java | 82 +++
src/main/java/org/keycloak/protocol/cas/CASLoginProtocolFactory.java | 124 +++++
src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java | 53 ++
src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java | 71 +++
pom.xml | 107 ++++
src/main/java/org/keycloak/protocol/cas/endpoints/LogoutEndpoint.java | 62 ++
src/main/java/org/keycloak/protocol/cas/installation/KeycloakCASClientInstallation.java | 77 +++
src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java | 38 +
src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper | 22
src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java | 60 ++
src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java | 191 ++++++++
src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider | 18
src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java | 127 +++++
src/main/java/org/keycloak/protocol/cas/endpoints/AuthorizationEndpoint.java | 106 ++++
18 files changed, 1,368 insertions(+), 0 deletions(-)
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..ec4165e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0"?>
+<!--
+ ~ 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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-protocol-cas</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ <name>Keycloak CAS Protocol</name>
+ <description />
+
+ <properties>
+ <keycloak.version>2.5.1.Final</keycloak.version>
+ <jboss.logging.version>3.3.0.Final</jboss.logging.version>
+ <jboss.logging.tools.version>2.0.1.Final</jboss.logging.tools.version>
+ <junit.version>4.12</junit.version>
+
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${keycloak.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <version>${keycloak.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi-private</artifactId>
+ <version>${keycloak.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ <version>${jboss.logging.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging-annotations</artifactId>
+ <version>${jboss.logging.tools.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging-processor</artifactId>
+ <version>${jboss.logging.tools.version}</version>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-services</artifactId>
+ <version>${keycloak.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>${junit.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.1</version>
+ <configuration>
+ <source>${maven.compiler.source}</source>
+ <target>${maven.compiler.target}</target>
+ <compilerArgument>
+ -AgeneratedTranslationFilesPath=${project.build.directory}/generated-translation-files
+ </compilerArgument>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java
new file mode 100644
index 0000000..10c9b5d
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java
@@ -0,0 +1,127 @@
+package org.keycloak.protocol.cas;
+
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.*;
+import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.managers.ResourceAdminManager;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.net.URI;
+
+public class CASLoginProtocol implements LoginProtocol {
+ public static final String LOGIN_PROTOCOL = "cas";
+
+ public static final String SERVICE_PARAM = "service";
+ public static final String RENEW_PARAM = "renew";
+ public static final String GATEWAY_PARAM = "gateway";
+ public static final String TICKET_PARAM = "ticket";
+ public static final String FORMAT_PARAM = "format";
+
+ public static final String TICKET_RESPONSE_PARAM = "ticket";
+
+ public static final String SERVICE_TICKET_PREFIX = "ST-";
+
+ protected KeycloakSession session;
+ protected RealmModel realm;
+ protected UriInfo uriInfo;
+ protected HttpHeaders headers;
+ protected EventBuilder event;
+ private boolean requireReauth;
+
+ public CASLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, EventBuilder event, boolean requireReauth) {
+ this.session = session;
+ this.realm = realm;
+ this.uriInfo = uriInfo;
+ this.headers = headers;
+ this.event = event;
+ this.requireReauth = requireReauth;
+ }
+
+ public CASLoginProtocol() {
+ }
+
+ @Override
+ public CASLoginProtocol setSession(KeycloakSession session) {
+ this.session = session;
+ return this;
+ }
+
+ @Override
+ public CASLoginProtocol setRealm(RealmModel realm) {
+ this.realm = realm;
+ return this;
+ }
+
+ @Override
+ public CASLoginProtocol setUriInfo(UriInfo uriInfo) {
+ this.uriInfo = uriInfo;
+ return this;
+ }
+
+ @Override
+ public CASLoginProtocol setHttpHeaders(HttpHeaders headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ @Override
+ public CASLoginProtocol setEventBuilder(EventBuilder event) {
+ this.event = event;
+ return this;
+ }
+
+ @Override
+ public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
+ ClientSessionModel clientSession = accessCode.getClientSession();
+
+ String service = clientSession.getRedirectUri();
+ //TODO validate service
+ accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name());
+ KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(service);
+ uriBuilder.queryParam(TICKET_RESPONSE_PARAM, SERVICE_TICKET_PREFIX + accessCode.getCode());
+
+ URI redirectUri = uriBuilder.build();
+
+ Response.ResponseBuilder location = Response.status(302).location(redirectUri);
+ return location.build();
+ }
+
+ @Override
+ public Response sendError(ClientSessionModel clientSession, Error error) {
+ return Response.serverError().entity(error).build();
+ }
+
+ @Override
+ public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
+ ClientModel client = clientSession.getClient();
+ new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, client, clientSession);
+ }
+
+ @Override
+ public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
+ // todo oidc redirect support
+ throw new RuntimeException("NOT IMPLEMENTED");
+ }
+
+ @Override
+ public Response finishLogout(UserSessionModel userSession) {
+ event.event(EventType.LOGOUT);
+ event.user(userSession.getUser()).session(userSession).success();
+ return Response.ok().build();
+ }
+
+ @Override
+ public boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession) {
+ return requireReauth;
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolFactory.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolFactory.java
new file mode 100644
index 0000000..57745b8
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolFactory.java
@@ -0,0 +1,124 @@
+package org.keycloak.protocol.cas;
+
+import org.jboss.logging.Logger;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.*;
+import org.keycloak.protocol.AbstractLoginProtocolFactory;
+import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.protocol.ProtocolMapperUtils;
+import org.keycloak.protocol.cas.mappers.FullNameMapper;
+import org.keycloak.protocol.cas.mappers.UserAttributeMapper;
+import org.keycloak.protocol.cas.mappers.UserPropertyMapper;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.JSON_TYPE;
+import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME;
+
+public class CASLoginProtocolFactory extends AbstractLoginProtocolFactory {
+ private static final Logger logger = Logger.getLogger(CASLoginProtocolFactory.class);
+
+ public static final String EMAIL = "email";
+ public static final String EMAIL_VERIFIED = "email verified";
+ public static final String GIVEN_NAME = "given name";
+ public static final String FAMILY_NAME = "family name";
+ public static final String FULL_NAME = "full name";
+ public static final String LOCALE = "locale";
+
+ public static final String EMAIL_CONSENT_TEXT = "${email}";
+ public static final String EMAIL_VERIFIED_CONSENT_TEXT = "${emailVerified}";
+ public static final String GIVEN_NAME_CONSENT_TEXT = "${givenName}";
+ public static final String FAMILY_NAME_CONSENT_TEXT = "${familyName}";
+ public static final String FULL_NAME_CONSENT_TEXT = "${fullName}";
+ public static final String LOCALE_CONSENT_TEXT = "${locale}";
+
+ @Override
+ public LoginProtocol create(KeycloakSession session) {
+ return new CASLoginProtocol().setSession(session);
+ }
+
+ @Override
+ public List<ProtocolMapperModel> getBuiltinMappers() {
+ return builtins;
+ }
+
+ @Override
+ public List<ProtocolMapperModel> getDefaultBuiltinMappers() {
+ return defaultBuiltins;
+ }
+
+ static List<ProtocolMapperModel> builtins = new ArrayList<>();
+ static List<ProtocolMapperModel> defaultBuiltins = new ArrayList<>();
+
+ static {
+ ProtocolMapperModel model;
+
+ model = UserPropertyMapper.create(EMAIL, "email", "mail", "String",
+ true, EMAIL_CONSENT_TEXT);
+ builtins.add(model);
+ defaultBuiltins.add(model);
+ model = UserPropertyMapper.create(GIVEN_NAME, "firstName", "givenName", "String",
+ true, GIVEN_NAME_CONSENT_TEXT);
+ builtins.add(model);
+ defaultBuiltins.add(model);
+ model = UserPropertyMapper.create(FAMILY_NAME, "lastName", "sn", "String",
+ true, FAMILY_NAME_CONSENT_TEXT);
+ builtins.add(model);
+ defaultBuiltins.add(model);
+ model = UserPropertyMapper.create(EMAIL_VERIFIED,
+ "emailVerified",
+ "emailVerified", "boolean",
+ false, EMAIL_VERIFIED_CONSENT_TEXT);
+ builtins.add(model);
+ model = UserAttributeMapper.create(LOCALE,
+ "locale",
+ "locale", "String",
+ false, LOCALE_CONSENT_TEXT,
+ false);
+ builtins.add(model);
+
+ model = FullNameMapper.create(FULL_NAME, "cn",
+ true, FULL_NAME_CONSENT_TEXT);
+ builtins.add(model);
+ defaultBuiltins.add(model);
+ }
+
+ @Override
+ protected void addDefaults(ClientModel client) {
+ for (ProtocolMapperModel model : defaultBuiltins) client.addProtocolMapper(model);
+ }
+
+ @Override
+ public Object createProtocolEndpoint(RealmModel realm, EventBuilder event) {
+ return new CASLoginProtocolService(realm, event);
+ }
+
+ @Override
+ public String getId() {
+ return CASLoginProtocol.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);
+ }
+
+ if (rep.getAdminUrl() == null && rep.getRootUrl() != null) {
+ newClient.setManagementUrl(rep.getRootUrl());
+ }
+ }
+
+ @Override
+ public void setupTemplateDefaults(ClientTemplateRepresentation clientRep, ClientTemplateModel newClient) {
+
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java
new file mode 100644
index 0000000..7db732f
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java
@@ -0,0 +1,92 @@
+package org.keycloak.protocol.cas;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.cas.endpoints.AuthorizationEndpoint;
+import org.keycloak.protocol.cas.endpoints.LogoutEndpoint;
+import org.keycloak.protocol.cas.endpoints.ServiceValidateEndpoint;
+import org.keycloak.protocol.cas.endpoints.ValidateEndpoint;
+import org.keycloak.services.resources.RealmsResource;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+public class CASLoginProtocolService {
+ private RealmModel realm;
+ private EventBuilder event;
+
+ @Context
+ private UriInfo uriInfo;
+
+ @Context
+ private KeycloakSession session;
+
+ @Context
+ private HttpHeaders headers;
+
+ @Context
+ private HttpRequest request;
+
+ public CASLoginProtocolService(RealmModel realm, EventBuilder event) {
+ this.realm = realm;
+ this.event = event;
+ }
+
+ public static UriBuilder serviceBaseUrl(UriBuilder baseUriBuilder) {
+ return baseUriBuilder.path(RealmsResource.class).path("{realm}/protocol/" + CASLoginProtocol.LOGIN_PROTOCOL);
+ }
+
+ @Path("login")
+ public Object login() {
+ AuthorizationEndpoint endpoint = new AuthorizationEndpoint(realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
+ }
+
+ @Path("logout")
+ public Object logout() {
+ LogoutEndpoint endpoint = new LogoutEndpoint(realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
+ }
+
+ @Path("validate")
+ public Object validate() {
+ ValidateEndpoint endpoint = new ValidateEndpoint(realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
+ }
+
+ @Path("serviceValidate")
+ public Object serviceValidate() {
+ ServiceValidateEndpoint endpoint = new ServiceValidateEndpoint(realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
+ }
+
+ @Path("proxyValidate")
+ public Object proxyValidate() {
+ return null;
+ }
+
+ @Path("proxy")
+ public Object proxy() {
+ return null;
+ }
+
+ @Path("p3/serviceValidate")
+ public Object p3ServiceValidate() {
+ return serviceValidate();
+ }
+
+ @Path("p3/proxyValidate")
+ public Object p3ProxyValidate() {
+ return proxyValidate();
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/AuthorizationEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/AuthorizationEndpoint.java
new file mode 100644
index 0000000..a3d80df
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/endpoints/AuthorizationEndpoint.java
@@ -0,0 +1,106 @@
+package org.keycloak.protocol.cas.endpoints;
+
+import org.jboss.logging.Logger;
+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.ClientSessionModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.AuthorizationEndpointBase;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.services.ErrorPageException;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.util.CacheControlUtil;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+public class AuthorizationEndpoint extends AuthorizationEndpointBase {
+ private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class);
+
+ private ClientModel client;
+ private ClientSessionModel clientSession;
+ private String redirectUri;
+
+ public AuthorizationEndpoint(RealmModel realm, EventBuilder event) {
+ super(realm, event);
+ event.event(EventType.LOGIN);
+ }
+
+ @GET
+ public Response build() {
+ MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
+ String service = params.getFirst(CASLoginProtocol.SERVICE_PARAM);
+ boolean renew = "true".equalsIgnoreCase(params.getFirst(CASLoginProtocol.RENEW_PARAM));
+ boolean gateway = "true".equalsIgnoreCase(params.getFirst(CASLoginProtocol.GATEWAY_PARAM));
+
+ checkSsl();
+ checkRealm();
+ checkClient(service);
+
+ createClientSession();
+ // So back button doesn't work
+ CacheControlUtil.noBackButtonCacheControlHeader();
+
+ this.event.event(EventType.LOGIN);
+ return handleBrowserAuthenticationRequest(clientSession, new CASLoginProtocol(session, realm, uriInfo, headers, event, renew), gateway, false);
+ }
+
+ private void checkSsl() {
+ if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
+ event.error(Errors.SSL_REQUIRED);
+ throw new ErrorPageException(session, Messages.HTTPS_REQUIRED);
+ }
+ }
+
+ private void checkRealm() {
+ if (!realm.isEnabled()) {
+ event.error(Errors.REALM_DISABLED);
+ throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED);
+ }
+ }
+
+ private void checkClient(String service) {
+ if (service == null) {
+ event.error(Errors.INVALID_REQUEST);
+ throw new ErrorPageException(session, Messages.MISSING_PARAMETER, CASLoginProtocol.SERVICE_PARAM);
+ }
+
+ client = realm.getClients().stream()
+ .filter(c -> CASLoginProtocol.LOGIN_PROTOCOL.equals(c.getProtocol()))
+ .filter(c -> RedirectUtils.verifyRedirectUri(uriInfo, service, realm, c) != null)
+ .findFirst().orElse(null);
+ if (client == null) {
+ event.error(Errors.CLIENT_NOT_FOUND);
+ throw new ErrorPageException(session, Messages.CLIENT_NOT_FOUND);
+ }
+
+ if (!client.isEnabled()) {
+ event.error(Errors.CLIENT_DISABLED);
+ throw new ErrorPageException(session, Messages.CLIENT_DISABLED);
+ }
+
+ if (client.isBearerOnly()) {
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorPageException(session, Messages.BEARER_ONLY);
+ }
+
+ redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, service, realm, client);
+
+ event.client(client.getClientId());
+ event.detail(Details.REDIRECT_URI, redirectUri);
+
+ session.getContext().setClient(client);
+ }
+
+ private void createClientSession() {
+ clientSession = session.sessions().createClientSession(realm, client);
+ clientSession.setAuthMethod(CASLoginProtocol.LOGIN_PROTOCOL);
+ clientSession.setRedirectUri(redirectUri);
+ clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/LogoutEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/LogoutEndpoint.java
new file mode 100644
index 0000000..c40da69
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/endpoints/LogoutEndpoint.java
@@ -0,0 +1,62 @@
+package org.keycloak.protocol.cas.endpoints;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.services.managers.AuthenticationManager;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+public class LogoutEndpoint {
+ private static final Logger logger = Logger.getLogger(org.keycloak.protocol.oidc.endpoints.LogoutEndpoint.class);
+
+ @Context
+ private KeycloakSession session;
+
+ @Context
+ private ClientConnection clientConnection;
+
+ @Context
+ private HttpRequest request;
+
+ @Context
+ private HttpHeaders headers;
+
+ @Context
+ private UriInfo uriInfo;
+
+ private RealmModel realm;
+ private EventBuilder event;
+
+ public LogoutEndpoint(RealmModel realm, EventBuilder event) {
+ this.realm = realm;
+ this.event = event;
+ }
+
+ @GET
+ @NoCache
+ public Response logout() {
+
+ AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, false);
+ if (authResult != null) {
+ UserSessionModel userSession = authResult.getSession();
+ userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, CASLoginProtocol.LOGIN_PROTOCOL);
+
+ logger.debug("Initiating CAS browser logout");
+ Response response = AuthenticationManager.browserLogout(session, realm, authResult.getSession(), uriInfo, clientConnection, headers);
+ logger.debug("finishing CAS browser logout");
+ return response;
+ }
+ return Response.ok().build();
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java
new file mode 100644
index 0000000..7b6a77e
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java
@@ -0,0 +1,53 @@
+package org.keycloak.protocol.cas.endpoints;
+
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.managers.ClientSessionCode;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.Set;
+
+public class ServiceValidateEndpoint extends ValidateEndpoint {
+ public ServiceValidateEndpoint(RealmModel realm, EventBuilder event) {
+ super(realm, event);
+ }
+
+ @Override
+ protected Response successResponse() {
+ UserSessionModel userSession = clientSession.getUserSession();
+
+ Set<ProtocolMapperModel> mappings = new ClientSessionCode(session, realm, clientSession).getRequestedProtocolMappers();
+ KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+ for (ProtocolMapperModel mapping : mappings) {
+ ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
+ }
+
+ return Response.ok()
+ .header(HttpHeaders.CONTENT_TYPE, (jsonFormat() ? MediaType.APPLICATION_JSON_TYPE : MediaType.APPLICATION_XML_TYPE).withCharset("utf-8"))
+ .entity("<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\n" +
+ " <cas:authenticationSuccess>\n" +
+ " <cas:user>" + userSession.getUser().getUsername() + "</cas:user>\n" +
+ " <cas:attributes>\n" +
+ " </cas:attributes>\n" +
+ " </cas:authenticationSuccess>\n" +
+ "</cas:serviceResponse>")
+ .build();
+ }
+
+ @Override
+ protected Response errorResponse(ErrorResponseException e) {
+ return super.errorResponse(e);
+ }
+
+ private boolean jsonFormat() {
+ return "json".equalsIgnoreCase(uriInfo.getQueryParameters().getFirst(CASLoginProtocol.FORMAT_PARAM));
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java
new file mode 100644
index 0000000..5727b50
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java
@@ -0,0 +1,191 @@
+package org.keycloak.protocol.cas.endpoints;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.OAuthErrorException;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.*;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.services.ErrorPageException;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.messages.Messages;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.core.*;
+
+public class ValidateEndpoint {
+ protected static final Logger logger = Logger.getLogger(org.keycloak.protocol.oidc.endpoints.LogoutEndpoint.class);
+
+ private static final String RESPONSE_OK = "yes\n";
+ private static final String RESPONSE_FAILED = "no\n";
+
+ @Context
+ protected KeycloakSession session;
+
+ @Context
+ protected ClientConnection clientConnection;
+
+ @Context
+ protected HttpRequest request;
+
+ @Context
+ protected HttpHeaders headers;
+
+ @Context
+ protected UriInfo uriInfo;
+
+ protected RealmModel realm;
+ protected EventBuilder event;
+ protected ClientModel client;
+ protected ClientSessionModel clientSession;
+
+ public ValidateEndpoint(RealmModel realm, EventBuilder event) {
+ this.realm = realm;
+ this.event = event;
+ }
+
+ @GET
+ @NoCache
+ public Response build() {
+ MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
+ String service = params.getFirst(CASLoginProtocol.SERVICE_PARAM);
+ String ticket = params.getFirst(CASLoginProtocol.TICKET_PARAM);
+ boolean renew = "true".equalsIgnoreCase(params.getFirst(CASLoginProtocol.RENEW_PARAM));
+
+ event.event(EventType.CODE_TO_TOKEN);
+
+ try {
+ checkSsl();
+ checkRealm();
+ checkClient(service);
+
+ checkTicket(ticket, renew);
+
+ event.success();
+ return successResponse();
+ } catch (ErrorResponseException e) {
+ return errorResponse(e);
+ }
+ }
+
+ protected Response successResponse() {
+ return Response.ok(RESPONSE_OK).type(MediaType.TEXT_PLAIN).build();
+ }
+
+ protected Response errorResponse(ErrorResponseException e) {
+ return Response.status(Response.Status.UNAUTHORIZED).entity(RESPONSE_FAILED).type(MediaType.TEXT_PLAIN).build();
+ }
+
+ private void checkSsl() {
+ if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN);
+ }
+ }
+
+ private void checkRealm() {
+ if (!realm.isEnabled()) {
+ throw new ErrorResponseException("access_denied", "Realm not enabled", Response.Status.FORBIDDEN);
+ }
+ }
+
+ private void checkClient(String service) {
+ if (service == null) {
+ event.error(Errors.INVALID_REQUEST);
+ throw new ErrorPageException(session, Messages.MISSING_PARAMETER, CASLoginProtocol.SERVICE_PARAM);
+ }
+
+ client = realm.getClients().stream()
+ .filter(c -> CASLoginProtocol.LOGIN_PROTOCOL.equals(c.getProtocol()))
+ .filter(c -> RedirectUtils.verifyRedirectUri(uriInfo, service, realm, c) != null)
+ .findFirst().orElse(null);
+ if (client == null) {
+ event.error(Errors.CLIENT_NOT_FOUND);
+ throw new ErrorPageException(session, Messages.CLIENT_NOT_FOUND);
+ }
+
+ if (!client.isEnabled()) {
+ event.error(Errors.CLIENT_DISABLED);
+ throw new ErrorPageException(session, Messages.CLIENT_DISABLED);
+ }
+
+ if (client.isBearerOnly()) {
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorPageException(session, Messages.BEARER_ONLY);
+ }
+
+ event.client(client.getClientId());
+
+ session.getContext().setClient(client);
+ }
+
+ private void checkTicket(String ticket, boolean requireReauth) {
+ if (ticket == null || !ticket.startsWith(CASLoginProtocol.SERVICE_TICKET_PREFIX)) {
+ event.error(Errors.INVALID_CODE);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Missing or invalid parameter: " + CASLoginProtocol.TICKET_PARAM, Response.Status.BAD_REQUEST);
+ }
+
+ String code = ticket.substring(CASLoginProtocol.SERVICE_TICKET_PREFIX.length());
+
+ ClientSessionCode.ParseResult parseResult = ClientSessionCode.parseResult(code, session, realm);
+ if (parseResult.isClientSessionNotFound() || parseResult.isIllegalHash()) {
+ String[] parts = code.split("\\.");
+ if (parts.length == 2) {
+ event.detail(Details.CODE_ID, parts[1]);
+ }
+ event.error(Errors.INVALID_CODE);
+ if (parseResult.getClientSession() != null) {
+ session.sessions().removeClientSession(realm, parseResult.getClientSession());
+ }
+ throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code not valid", Response.Status.BAD_REQUEST);
+ }
+
+ clientSession = parseResult.getClientSession();
+ event.detail(Details.CODE_ID, clientSession.getId());
+
+ if (!parseResult.getCode().isValid(ClientSessionModel.Action.CODE_TO_TOKEN.name(), ClientSessionCode.ActionType.CLIENT)) {
+ event.error(Errors.INVALID_CODE);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code is expired", Response.Status.BAD_REQUEST);
+ }
+
+ parseResult.getCode().setAction(null);
+
+ UserSessionModel userSession = clientSession.getUserSession();
+
+ if (userSession == null) {
+ event.error(Errors.USER_SESSION_NOT_FOUND);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User session not found", Response.Status.BAD_REQUEST);
+ }
+
+ UserModel user = userSession.getUser();
+ if (user == null) {
+ event.error(Errors.USER_NOT_FOUND);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User not found", Response.Status.BAD_REQUEST);
+ }
+ if (!user.isEnabled()) {
+ event.error(Errors.USER_DISABLED);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User disabled", Response.Status.BAD_REQUEST);
+ }
+
+ event.user(userSession.getUser());
+ event.session(userSession.getId());
+
+ if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
+ event.error(Errors.INVALID_CODE);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Auth error", Response.Status.BAD_REQUEST);
+ }
+
+ if (!AuthenticationManager.isSessionValid(realm, userSession)) {
+ event.error(Errors.USER_SESSION_NOT_FOUND);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Session not active", Response.Status.BAD_REQUEST);
+ }
+
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/installation/KeycloakCASClientInstallation.java b/src/main/java/org/keycloak/protocol/cas/installation/KeycloakCASClientInstallation.java
new file mode 100644
index 0000000..eb0785d
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/installation/KeycloakCASClientInstallation.java
@@ -0,0 +1,77 @@
+package org.keycloak.protocol.cas.installation;
+
+import org.keycloak.Config;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.ClientInstallationProvider;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+public class KeycloakCASClientInstallation implements ClientInstallationProvider {
+
+ @Override
+ public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) {
+ return Response.ok("{}", MediaType.TEXT_PLAIN_TYPE).build();
+ }
+
+ @Override
+ public String getProtocol() {
+ return CASLoginProtocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Keycloak CAS JSON";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "keycloak.json file used by the Keycloak CAS client adapter to configure clients. This must be saved to a keycloak.json file and put in your WEB-INF directory of your WAR file. You may also want to tweak this file after you download it.";
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public ClientInstallationProvider create(KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public String getId() {
+ return "keycloak-cas-keycloak-json";
+ }
+
+ @Override
+ public boolean isDownloadOnly() {
+ return false;
+ }
+
+ @Override
+ public String getFilename() {
+ return "keycloak.json";
+ }
+
+ @Override
+ public String getMediaType() {
+ return MediaType.APPLICATION_JSON;
+ }
+
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java
new file mode 100644
index 0000000..8c61a4e
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java
@@ -0,0 +1,38 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+
+public abstract class AbstractCASProtocolMapper implements ProtocolMapper {
+ public static final String TOKEN_MAPPER_CATEGORY = "Token mapper";
+
+ @Override
+ public String getProtocol() {
+ return CASLoginProtocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public final ProtocolMapper create(KeycloakSession session) {
+ throw new RuntimeException("UNSUPPORTED METHOD");
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return TOKEN_MAPPER_CATEGORY;
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java
new file mode 100644
index 0000000..59a9384
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java
@@ -0,0 +1,60 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.protocol.ProtocolMapperUtils;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.JSON_TYPE;
+import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME;
+
+public class FullNameMapper extends AbstractCASProtocolMapper {
+ private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ static {
+ OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
+ }
+
+ public static final String PROVIDER_ID = "cas-full-name-mapper";
+
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "User's full name";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Maps the user's first and last name to the OpenID Connect 'name' claim. Format is <first> + ' ' + <last>";
+ }
+
+ public static ProtocolMapperModel create(String name, String tokenClaimName,
+ boolean consentRequired, String consentText) {
+ ProtocolMapperModel mapper = new ProtocolMapperModel();
+ mapper.setName(name);
+ mapper.setProtocolMapper(PROVIDER_ID);
+ mapper.setProtocol(CASLoginProtocol.LOGIN_PROTOCOL);
+ mapper.setConsentRequired(consentRequired);
+ mapper.setConsentText(consentText);
+ Map<String, String> config = new HashMap<String, String>();
+ config.put(TOKEN_CLAIM_NAME, tokenClaimName);
+ mapper.setConfig(config);
+ return mapper;
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java
new file mode 100644
index 0000000..665a6e8
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java
@@ -0,0 +1,70 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class GroupMembershipMapper extends AbstractCASProtocolMapper {
+ private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ static {
+ OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
+ ProviderConfigProperty property1 = new ProviderConfigProperty();
+ property1.setName("full.path");
+ property1.setLabel("Full group path");
+ property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ property1.setDefaultValue("true");
+ property1.setHelpText("Include full path to group i.e. /top/level1/level2, false will just specify the group name");
+ configProperties.add(property1);
+ }
+
+ public static final String PROVIDER_ID = "cas-group-membership-mapper";
+
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Group Membership";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Map user group membership";
+ }
+
+ public static boolean useFullPath(ProtocolMapperModel mappingModel) {
+ return "true".equals(mappingModel.getConfig().get("full.path"));
+ }
+
+ public static ProtocolMapperModel create(String name, String tokenClaimName,
+ boolean consentRequired, String consentText, boolean fullPath) {
+ ProtocolMapperModel mapper = new ProtocolMapperModel();
+ mapper.setName(name);
+ mapper.setProtocolMapper(PROVIDER_ID);
+ mapper.setProtocol(CASLoginProtocol.LOGIN_PROTOCOL);
+ mapper.setConsentRequired(consentRequired);
+ mapper.setConsentText(consentText);
+ Map<String, String> config = new HashMap<String, String>();
+ config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, tokenClaimName);
+ config.put("full.path", Boolean.toString(fullPath));
+ mapper.setConfig(config);
+
+ return mapper;
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java b/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java
new file mode 100644
index 0000000..34028ac
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java
@@ -0,0 +1,50 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class HardcodedClaim extends AbstractCASProtocolMapper {
+ private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ public static final String CLAIM_VALUE = "claim.value";
+
+ static {
+ OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
+
+ ProviderConfigProperty property = new ProviderConfigProperty();
+ property.setName(CLAIM_VALUE);
+ property.setLabel("Claim value");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText("Value of the claim you want to hard code. 'true' and 'false can be used for boolean values.");
+ configProperties.add(property);
+
+ OIDCAttributeMapperHelper.addJsonTypeConfig(configProperties);
+ }
+
+ public static final String PROVIDER_ID = "cas-hardcoded-claim-mapper";
+
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Hardcoded claim";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Hardcode a claim into the token.";
+ }
+
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java
new file mode 100644
index 0000000..5206126
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java
@@ -0,0 +1,82 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.protocol.ProtocolMapperUtils;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.JSON_TYPE;
+import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME;
+
+public class UserAttributeMapper extends AbstractCASProtocolMapper {
+ private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ static {
+ ProviderConfigProperty property;
+ property = new ProviderConfigProperty();
+ property.setName(ProtocolMapperUtils.USER_ATTRIBUTE);
+ property.setLabel(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_LABEL);
+ property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT);
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(property);
+ OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
+ OIDCAttributeMapperHelper.addJsonTypeConfig(configProperties);
+
+ property = new ProviderConfigProperty();
+ property.setName(ProtocolMapperUtils.MULTIVALUED);
+ property.setLabel(ProtocolMapperUtils.MULTIVALUED_LABEL);
+ property.setHelpText(ProtocolMapperUtils.MULTIVALUED_HELP_TEXT);
+ property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ configProperties.add(property);
+
+ }
+
+ public static final String PROVIDER_ID = "cas-usermodel-attribute-mapper";
+
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "User Attribute";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Map a custom user attribute to a token claim.";
+ }
+
+ public static ProtocolMapperModel create(String name, String userAttribute,
+ String tokenClaimName, String claimType,
+ boolean consentRequired, String consentText, boolean multivalued) {
+ ProtocolMapperModel mapper = new ProtocolMapperModel();
+ mapper.setName(name);
+ mapper.setProtocolMapper(PROVIDER_ID);
+ mapper.setProtocol(CASLoginProtocol.LOGIN_PROTOCOL);
+ mapper.setConsentRequired(consentRequired);
+ mapper.setConsentText(consentText);
+ Map<String, String> config = new HashMap<String, String>();
+ config.put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute);
+ config.put(TOKEN_CLAIM_NAME, tokenClaimName);
+ config.put(JSON_TYPE, claimType);
+ if (multivalued) {
+ mapper.getConfig().put(ProtocolMapperUtils.MULTIVALUED, "true");
+ }
+ mapper.setConfig(config);
+ return mapper;
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java
new file mode 100644
index 0000000..21cdeb6
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java
@@ -0,0 +1,71 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.protocol.ProtocolMapperUtils;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.JSON_TYPE;
+import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME;
+
+public class UserPropertyMapper extends AbstractCASProtocolMapper {
+ private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ static {
+ ProviderConfigProperty property;
+ property = new ProviderConfigProperty();
+ property.setName(ProtocolMapperUtils.USER_ATTRIBUTE);
+ property.setLabel(ProtocolMapperUtils.USER_MODEL_PROPERTY_LABEL);
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText(ProtocolMapperUtils.USER_MODEL_PROPERTY_HELP_TEXT);
+ configProperties.add(property);
+ OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
+ OIDCAttributeMapperHelper.addJsonTypeConfig(configProperties);
+ }
+
+ public static final String PROVIDER_ID = "cas-usermodel-property-mapper";
+
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "User Property";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Map a built in user property (email, firstName, lastName) to a token claim.";
+ }
+
+ public static ProtocolMapperModel create(String name, String userAttribute,
+ String tokenClaimName, String claimType,
+ boolean consentRequired, String consentText) {
+ ProtocolMapperModel mapper = new ProtocolMapperModel();
+ mapper.setName(name);
+ mapper.setProtocolMapper(PROVIDER_ID);
+ mapper.setProtocol(CASLoginProtocol.LOGIN_PROTOCOL);
+ mapper.setConsentRequired(consentRequired);
+ mapper.setConsentText(consentText);
+ Map<String, String> config = new HashMap<String, String>();
+ config.put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute);
+ config.put(TOKEN_CLAIM_NAME, tokenClaimName);
+ config.put(JSON_TYPE, claimType);
+ mapper.setConfig(config);
+ return mapper;
+ }
+}
diff --git a/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider b/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider
new file mode 100644
index 0000000..ba8bf6e
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.protocol.cas.installation.KeycloakCASClientInstallation
diff --git a/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory b/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory
new file mode 100644
index 0000000..16e9190
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.protocol.cas.CASLoginProtocolFactory
diff --git a/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
new file mode 100644
index 0000000..46409eb
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+org.keycloak.protocol.cas.mappers.FullNameMapper
+org.keycloak.protocol.cas.mappers.GroupMembershipMapper
+org.keycloak.protocol.cas.mappers.HardcodedClaim
+org.keycloak.protocol.cas.mappers.UserAttributeMapper
+org.keycloak.protocol.cas.mappers.UserPropertyMapper
--
Gitblit v1.9.1