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

Jacek Kowalski
2023-11-24 e3e192bc4cb1fb4c7aa5eee57eab525500388ce7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package org.keycloak.protocol.cas.endpoints;
 
import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.protocol.cas.mappers.CASAttributeMapper;
import org.keycloak.protocol.cas.representations.CASErrorCode;
import org.keycloak.protocol.cas.utils.CASValidationException;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.DefaultClientSessionContext;
 
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
 
public abstract class AbstractValidateEndpoint {
    protected final Logger logger = Logger.getLogger(getClass());
    protected KeycloakSession session;
    protected RealmModel realm;
    protected EventBuilder event;
    protected ClientModel client;
    protected AuthenticatedClientSessionModel clientSession;
 
    public AbstractValidateEndpoint(KeycloakSession session, RealmModel realm, EventBuilder event) {
        this.session = session;
        this.realm = realm;
        this.event = event;
    }
 
    protected void checkSsl() {
        if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(session.getContext().getConnection())) {
            throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN);
        }
    }
 
    protected void checkRealm() {
        if (!realm.isEnabled()) {
            throw new CASValidationException(CASErrorCode.INTERNAL_ERROR, "Realm not enabled", Response.Status.FORBIDDEN);
        }
    }
 
    protected void checkClient(String service) {
        if (service == null) {
            event.error(Errors.INVALID_REQUEST);
            throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.SERVICE_PARAM, Response.Status.BAD_REQUEST);
        }
 
        event.detail(Details.REDIRECT_URI, service);
 
        client = realm.getClientsStream()
                .filter(c -> CASLoginProtocol.LOGIN_PROTOCOL.equals(c.getProtocol()))
                .filter(c -> RedirectUtils.verifyRedirectUri(session, service, c) != null)
                .findFirst().orElse(null);
        if (client == null) {
            event.error(Errors.CLIENT_NOT_FOUND);
            throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client not found", Response.Status.BAD_REQUEST);
        }
 
        if (!client.isEnabled()) {
            event.error(Errors.CLIENT_DISABLED);
            throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client disabled", Response.Status.BAD_REQUEST);
        }
 
        event.client(client.getClientId());
 
        session.getContext().setClient(client);
    }
 
    protected void checkTicket(String ticket, boolean requireReauth) {
        if (ticket == null) {
            event.error(Errors.INVALID_CODE);
            throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.TICKET_PARAM, Response.Status.BAD_REQUEST);
        }
        if (!ticket.startsWith(CASLoginProtocol.SERVICE_TICKET_PREFIX)) {
            event.error(Errors.INVALID_CODE);
            throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Malformed service ticket", Response.Status.BAD_REQUEST);
        }
 
        String code = ticket.substring(CASLoginProtocol.SERVICE_TICKET_PREFIX.length());
 
        OAuth2CodeParser.ParseResult parseResult = OAuth2CodeParser.parseCode(session, code, realm, event);
        if (parseResult.isIllegalCode()) {
            event.error(Errors.INVALID_CODE);
 
            // Attempt to use same code twice should invalidate existing clientSession
            AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
            if (clientSession != null) {
                clientSession.detachFromUserSession();
            }
 
            throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", Response.Status.BAD_REQUEST);
        }
 
        clientSession = parseResult.getClientSession();
 
        if (parseResult.isExpiredCode()) {
            event.error(Errors.EXPIRED_CODE);
            throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code is expired", Response.Status.BAD_REQUEST);
        }
 
        clientSession.setNote(CASLoginProtocol.SESSION_SERVICE_TICKET, ticket);
 
        if (requireReauth && AuthenticationManager.isSSOAuthentication(clientSession)) {
            event.error(Errors.SESSION_EXPIRED);
            throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Interactive authentication was requested but not performed", Response.Status.BAD_REQUEST);
        }
 
        UserSessionModel userSession = clientSession.getUserSession();
 
        if (userSession == null) {
            event.error(Errors.USER_SESSION_NOT_FOUND);
            throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User session not found", Response.Status.BAD_REQUEST);
        }
 
        UserModel user = userSession.getUser();
        if (user == null) {
            event.error(Errors.USER_NOT_FOUND);
            throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User not found", Response.Status.BAD_REQUEST);
        }
        if (!user.isEnabled()) {
            event.error(Errors.USER_DISABLED);
            throw new CASValidationException(CASErrorCode.INVALID_TICKET, "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 CASValidationException(CASErrorCode.INVALID_SERVICE, "Auth error", Response.Status.BAD_REQUEST);
        }
 
        if (!AuthenticationManager.isSessionValid(realm, userSession)) {
            event.error(Errors.USER_SESSION_NOT_FOUND);
            throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Session not active", Response.Status.BAD_REQUEST);
        }
    }
 
    protected Map<String, Object> getUserAttributes() {
        UserSessionModel userSession = clientSession.getUserSession();
        // CAS protocol does not support scopes, so pass null scopeParam
        ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, null, session);
 
        Set<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappersStream().collect(Collectors.toSet());
        KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
        Map<String, Object> attributes = new HashMap<>();
        for (ProtocolMapperModel mapping : mappings) {
            ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
            if (mapper instanceof CASAttributeMapper) {
                ((CASAttributeMapper) mapper).setAttribute(attributes, mapping, userSession, session, clientSessionCtx);
            }
        }
        return attributes;
    }
}