Commit 7f7e0cce authored by Matthias Piepkorn's avatar Matthias Piepkorn
Browse files

Add protocol SPIs, endpoints for login/logout/serviceValidate and some

claim mapper stubs
parent 8c35d0ab
<?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>
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() {
}
}
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) {
}
}
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();
}
}
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());
}
}
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