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