1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.core.logic;
20
21 import com.nimbusds.jwt.JWTClaimsSet;
22 import com.nimbusds.jwt.SignedJWT;
23 import com.nimbusds.oauth2.sdk.AuthorizationCode;
24 import java.lang.reflect.Method;
25 import java.text.ParseException;
26 import java.time.OffsetDateTime;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Optional;
31 import org.apache.commons.lang3.StringUtils;
32 import org.apache.commons.lang3.tuple.Pair;
33 import org.apache.syncope.common.lib.Attr;
34 import org.apache.syncope.common.lib.SyncopeClientException;
35 import org.apache.syncope.common.lib.oidc.OIDCLoginResponse;
36 import org.apache.syncope.common.lib.oidc.OIDCRequest;
37 import org.apache.syncope.common.lib.to.EntityTO;
38 import org.apache.syncope.common.lib.to.Item;
39 import org.apache.syncope.common.lib.to.UserTO;
40 import org.apache.syncope.common.lib.types.CipherAlgorithm;
41 import org.apache.syncope.common.lib.types.ClientExceptionType;
42 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
43 import org.apache.syncope.core.logic.oidc.NoOpSessionStore;
44 import org.apache.syncope.core.logic.oidc.OIDCC4UIContext;
45 import org.apache.syncope.core.logic.oidc.OIDCClientCache;
46 import org.apache.syncope.core.logic.oidc.OIDCUserManager;
47 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
48 import org.apache.syncope.core.persistence.api.dao.OIDCC4UIProviderDAO;
49 import org.apache.syncope.core.persistence.api.entity.OIDCC4UIProvider;
50 import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder;
51 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
52 import org.apache.syncope.core.spring.security.AuthContextUtils;
53 import org.apache.syncope.core.spring.security.AuthDataAccessor;
54 import org.apache.syncope.core.spring.security.Encryptor;
55 import org.pac4j.core.exception.http.WithLocationAction;
56 import org.pac4j.oidc.client.OidcClient;
57 import org.pac4j.oidc.credentials.OidcCredentials;
58 import org.pac4j.oidc.profile.OidcProfile;
59 import org.springframework.security.access.prepost.PreAuthorize;
60
61 public class OIDCC4UILogic extends AbstractTransactionalLogic<EntityTO> {
62
63 protected static final String JWT_CLAIM_OP_NAME = "OP_NAME";
64
65 protected static final String JWT_CLAIM_ID_TOKEN = "ID_TOKEN";
66
67 protected static final Encryptor ENCRYPTOR = Encryptor.getInstance();
68
69 protected final OIDCClientCache oidcClientCacheLogin;
70
71 protected final OIDCClientCache oidcClientCacheLogout;
72
73 protected final AuthDataAccessor authDataAccessor;
74
75 protected final AccessTokenDataBinder accessTokenDataBinder;
76
77 protected final OIDCC4UIProviderDAO opDAO;
78
79 protected final OIDCUserManager userManager;
80
81 public OIDCC4UILogic(
82 final OIDCClientCache oidcClientCacheLogin,
83 final OIDCClientCache oidcClientCacheLogout,
84 final AuthDataAccessor authDataAccessor,
85 final AccessTokenDataBinder accessTokenDataBinder,
86 final OIDCC4UIProviderDAO opDAO,
87 final OIDCUserManager userManager) {
88
89 this.oidcClientCacheLogin = oidcClientCacheLogin;
90 this.oidcClientCacheLogout = oidcClientCacheLogout;
91 this.authDataAccessor = authDataAccessor;
92 this.accessTokenDataBinder = accessTokenDataBinder;
93 this.opDAO = opDAO;
94 this.userManager = userManager;
95 }
96
97 protected OidcClient getOidcClient(
98 final OIDCClientCache oidcClientCache,
99 final OIDCC4UIProvider op,
100 final String callbackUrl) {
101
102 return oidcClientCache.get(op.getName()).orElseGet(() -> oidcClientCache.add(op, callbackUrl));
103 }
104
105 @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
106 public OIDCRequest createLoginRequest(final String redirectURI, final String opName) {
107
108 OIDCC4UIProvider op = Optional.ofNullable(opDAO.findByName(opName)).
109 orElseThrow(() -> new NotFoundException("OIDC Provider '" + opName + '\''));
110
111
112 OidcClient oidcClient = getOidcClient(oidcClientCacheLogin, op, redirectURI);
113
114
115 WithLocationAction action = oidcClient.getRedirectionAction(new OIDCC4UIContext(), NoOpSessionStore.INSTANCE).
116 map(WithLocationAction.class::cast).
117 orElseThrow(() -> {
118 SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
119 sce.getElements().add("No RedirectionAction generated for LoginRequest");
120 return sce;
121 });
122
123 OIDCRequest loginRequest = new OIDCRequest();
124 loginRequest.setLocation(action.getLocation());
125 return loginRequest;
126 }
127
128 @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
129 public OIDCLoginResponse login(final String redirectURI, final String authorizationCode, final String opName) {
130
131 OIDCC4UIProvider op = Optional.ofNullable(opDAO.findByName(opName)).
132 orElseThrow(() -> new NotFoundException("OIDC Provider '" + opName + '\''));
133
134
135 OidcClient oidcClient = getOidcClient(oidcClientCacheLogin, op, redirectURI);
136
137
138 String idTokenHint;
139 JWTClaimsSet idToken;
140 try {
141 OidcCredentials credentials = new OidcCredentials();
142 credentials.setCode(new AuthorizationCode(authorizationCode));
143
144 oidcClient.getAuthenticator().validate(credentials, new OIDCC4UIContext(), NoOpSessionStore.INSTANCE);
145
146 idToken = credentials.getIdToken().getJWTClaimsSet();
147 idTokenHint = credentials.getIdToken().serialize();
148 } catch (Exception e) {
149 LOG.error("While validating Token Response", e);
150 SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
151 sce.getElements().add(e.getMessage());
152 throw sce;
153 }
154
155
156 OIDCLoginResponse loginResp = new OIDCLoginResponse();
157 loginResp.setLogoutSupported(StringUtils.isNotBlank(op.getEndSessionEndpoint()));
158
159
160 String keyValue = idToken.getSubject();
161 for (Item item : op.getItems()) {
162 Attr attrTO = new Attr();
163 attrTO.setSchema(item.getExtAttrName());
164
165 String value = Optional.ofNullable(idToken.getClaim(item.getExtAttrName())).
166 map(Object::toString).
167 orElse(null);
168 if (value != null) {
169 attrTO.getValues().add(value);
170 loginResp.getAttrs().add(attrTO);
171 if (item.isConnObjectKey()) {
172 keyValue = value;
173 }
174 }
175 }
176
177 List<String> matchingUsers = Optional.ofNullable(keyValue).
178 map(k -> userManager.findMatchingUser(k, op.getConnObjectKeyItem().get())).
179 orElse(List.of());
180 LOG.debug("Found {} matching users for {}", matchingUsers.size(), keyValue);
181
182
183 String username;
184 if (matchingUsers.isEmpty()) {
185 if (op.isCreateUnmatching()) {
186 LOG.debug("No user matching {}, about to create", keyValue);
187
188 String defaultUsername = keyValue;
189 username = AuthContextUtils.callAsAdmin(AuthContextUtils.getDomain(),
190 () -> userManager.create(op, loginResp, defaultUsername));
191 } else if (op.isSelfRegUnmatching()) {
192 UserTO userTO = new UserTO();
193
194 userManager.fill(op, loginResp, userTO);
195
196 loginResp.getAttrs().clear();
197 loginResp.getAttrs().addAll(userTO.getPlainAttrs());
198 if (StringUtils.isNotBlank(userTO.getUsername())) {
199 loginResp.setUsername(userTO.getUsername());
200 } else {
201 loginResp.setUsername(keyValue);
202 }
203
204 loginResp.setSelfReg(true);
205
206 return loginResp;
207 } else {
208 throw new NotFoundException(Optional.ofNullable(keyValue).
209 map(value -> "User matching the provided value " + value).
210 orElse("User marching the provided claims"));
211 }
212 } else if (matchingUsers.size() > 1) {
213 throw new IllegalArgumentException("Several users match the provided value " + keyValue);
214 } else {
215 if (op.isUpdateMatching()) {
216 LOG.debug("About to update {} for {}", matchingUsers.get(0), keyValue);
217
218 username = AuthContextUtils.callAsAdmin(AuthContextUtils.getDomain(),
219 () -> userManager.update(matchingUsers.get(0), op, loginResp));
220 } else {
221 username = matchingUsers.get(0);
222 }
223 }
224
225 loginResp.setUsername(username);
226
227
228 Map<String, Object> claims = new HashMap<>();
229 claims.put(JWT_CLAIM_OP_NAME, opName);
230 claims.put(JWT_CLAIM_ID_TOKEN, idTokenHint);
231
232 byte[] authorities = null;
233 try {
234 authorities = ENCRYPTOR.encode(POJOHelper.serialize(
235 authDataAccessor.getAuthorities(loginResp.getUsername(), null)), CipherAlgorithm.AES).
236 getBytes();
237 } catch (Exception e) {
238 LOG.error("Could not fetch authorities", e);
239 }
240
241 Pair<String, OffsetDateTime> accessTokenInfo =
242 accessTokenDataBinder.create(loginResp.getUsername(), claims, authorities, true);
243 loginResp.setAccessToken(accessTokenInfo.getLeft());
244 loginResp.setAccessTokenExpiryTime(accessTokenInfo.getRight());
245
246 return loginResp;
247 }
248
249 @PreAuthorize("isAuthenticated() and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "'))")
250 public OIDCRequest createLogoutRequest(final String accessToken, final String redirectURI) {
251
252 JWTClaimsSet claimsSet;
253 try {
254 SignedJWT jwt = SignedJWT.parse(accessToken);
255 claimsSet = jwt.getJWTClaimsSet();
256 } catch (ParseException e) {
257 SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidAccessToken);
258 sce.getElements().add(e.getMessage());
259 throw sce;
260 }
261
262
263 OIDCC4UIProvider op = Optional.ofNullable(opDAO.findByName((String) claimsSet.getClaim(JWT_CLAIM_OP_NAME))).
264 orElseThrow(() -> new NotFoundException(""
265 + "OIDC Provider '" + claimsSet.getClaim(JWT_CLAIM_OP_NAME) + '\''));
266 OidcClient oidcClient = getOidcClient(oidcClientCacheLogout, op, redirectURI);
267
268
269 OidcProfile profile = new OidcProfile();
270 profile.setIdTokenString((String) claimsSet.getClaim(JWT_CLAIM_ID_TOKEN));
271
272 WithLocationAction action = oidcClient.getLogoutAction(
273 new OIDCC4UIContext(),
274 NoOpSessionStore.INSTANCE,
275 profile,
276 redirectURI).
277 map(WithLocationAction.class::cast).
278 orElseThrow(() -> {
279 SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
280 sce.getElements().add("No RedirectionAction generated for LogoutRequest");
281 return sce;
282 });
283
284 OIDCRequest logoutRequest = new OIDCRequest();
285 logoutRequest.setLocation(action.getLocation());
286 return logoutRequest;
287 }
288
289 @Override
290 protected EntityTO resolveReference(
291 final Method method, final Object... args) throws UnresolvedReferenceException {
292
293 throw new UnresolvedReferenceException();
294 }
295 }