mirror of https://github.com/jacekkow/keycloak-protocol-cas

Matthias Piepkorn
2017-01-27 7f7e0cce1b38b199d9a8c22a5e85e18e5c37c7c5
Add protocol SPIs, endpoints for login/logout/serviceValidate and some
claim mapper stubs
18 files added
1368 ■■■■■ changed files
pom.xml 107 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java 127 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/CASLoginProtocolFactory.java 124 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java 92 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/endpoints/AuthorizationEndpoint.java 106 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/endpoints/LogoutEndpoint.java 62 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java 53 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java 191 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/installation/KeycloakCASClientInstallation.java 77 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java 38 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java 60 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java 70 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java 50 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java 82 ●●●●● patch | view | raw | blame | history
src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java 71 ●●●●● patch | view | raw | blame | history
src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider 18 ●●●●● patch | view | raw | blame | history
src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory 18 ●●●●● patch | view | raw | blame | history
src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper 22 ●●●●● patch | view | raw | blame | history
pom.xml
New file
@@ -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>
src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java
New file
@@ -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() {
    }
}
src/main/java/org/keycloak/protocol/cas/CASLoginProtocolFactory.java
New file
@@ -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) {
    }
}
src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java
New file
@@ -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();
    }
}
src/main/java/org/keycloak/protocol/cas/endpoints/AuthorizationEndpoint.java
New file
@@ -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());
    }
}
src/main/java/org/keycloak/protocol/cas/endpoints/LogoutEndpoint.java
New file
@@ -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();
    }
}
src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java
New file
@@ -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));
    }
}
src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java
New file
@@ -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);
        }
    }
}
src/main/java/org/keycloak/protocol/cas/installation/KeycloakCASClientInstallation.java
New file
@@ -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;
    }
}
src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java
New file
@@ -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;
    }
}
src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java
New file
@@ -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;
    }
}
src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java
New file
@@ -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;
    }
}
src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java
New file
@@ -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.";
    }
}
src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java
New file
@@ -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;
    }
}
src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java
New file
@@ -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;
    }
}
src/main/resources/META-INF/services/org.keycloak.protocol.ClientInstallationProvider
New file
@@ -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
src/main/resources/META-INF/services/org.keycloak.protocol.LoginProtocolFactory
New file
@@ -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
src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
New file
@@ -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