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