blob: 6624cf9382922204793736b9bf1b40979ffd546d [file] [log] [blame]
margarethaa452c5e2018-04-25 22:48:09 +02001package de.ids_mannheim.korap.oauth2.service;
margaretha31a9f522018-04-03 20:40:45 +02002
margaretha6374f722018-04-17 18:45:57 +02003import java.net.MalformedURLException;
4import java.net.URI;
5import java.net.URISyntaxException;
6import java.net.URL;
margaretha8d804f62018-04-10 12:39:56 +02007import java.sql.SQLException;
margaretha230effb2018-11-29 17:28:18 +01008import java.util.ArrayList;
9import java.util.Collections;
margaretha7f5071f2018-08-14 15:58:51 +020010import java.util.List;
margaretha8d804f62018-04-10 12:39:56 +020011
margaretha31a9f522018-04-03 20:40:45 +020012import org.apache.commons.validator.routines.UrlValidator;
13import org.springframework.beans.factory.annotation.Autowired;
14import org.springframework.stereotype.Service;
15
margaretha6d61a552018-04-10 19:26:44 +020016import de.ids_mannheim.korap.config.FullConfiguration;
margaretha8d804f62018-04-10 12:39:56 +020017import de.ids_mannheim.korap.dao.AdminDao;
margaretha33fa3d92018-07-26 13:50:17 +020018import de.ids_mannheim.korap.encryption.RandomCodeGenerator;
margaretha31a9f522018-04-03 20:40:45 +020019import de.ids_mannheim.korap.exceptions.KustvaktException;
20import de.ids_mannheim.korap.exceptions.StatusCodes;
21import de.ids_mannheim.korap.interfaces.EncryptionIface;
margarethaa452c5e2018-04-25 22:48:09 +020022import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
23import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
margaretha7f5071f2018-08-14 15:58:51 +020024import de.ids_mannheim.korap.oauth2.dao.AccessTokenDao;
margaretha34954472018-10-24 20:05:17 +020025import de.ids_mannheim.korap.oauth2.dao.AuthorizationDao;
margarethaa452c5e2018-04-25 22:48:09 +020026import de.ids_mannheim.korap.oauth2.dao.OAuth2ClientDao;
margarethaf370f542018-08-23 18:51:49 +020027import de.ids_mannheim.korap.oauth2.dao.RefreshTokenDao;
margaretha835178d2018-08-15 19:04:03 +020028import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientDto;
29import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientInfoDto;
margaretha230effb2018-11-29 17:28:18 +010030import de.ids_mannheim.korap.oauth2.dto.OAuth2UserClientDto;
margaretha7f5071f2018-08-14 15:58:51 +020031import de.ids_mannheim.korap.oauth2.entity.AccessToken;
32import de.ids_mannheim.korap.oauth2.entity.Authorization;
margarethaa452c5e2018-04-25 22:48:09 +020033import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
margarethaf370f542018-08-23 18:51:49 +020034import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
margaretha21d32962019-11-14 17:08:15 +010035import de.ids_mannheim.korap.utils.ParameterChecker;
margaretha31a9f522018-04-03 20:40:45 +020036import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
37
margarethaa452c5e2018-04-25 22:48:09 +020038/**
margaretha398f4722019-01-09 19:07:20 +010039 * Defines business logic related to OAuth2 client including
40 * client registration and client authentication.
41 *
margarethaa452c5e2018-04-25 22:48:09 +020042 * According to RFC 6749, an authorization server MUST:
margarethaa0486272018-04-12 19:59:31 +020043 * <ul>
44 * <li>
45 * require client authentication for confidential clients or for any
margarethaa452c5e2018-04-25 22:48:09 +020046 * client that was issued client credentials (or with other
47 * authentication
margarethaa0486272018-04-12 19:59:31 +020048 * requirements),
49 * </li>
50 *
51 * <li>authenticate the client if client authentication is included
52 * </li>
53 * </ul>
54 *
55 * @author margaretha
56 *
57 */
margaretha31a9f522018-04-03 20:40:45 +020058@Service
59public class OAuth2ClientService {
60
61 @Autowired
62 private OAuth2ClientDao clientDao;
63 @Autowired
margaretha7f5071f2018-08-14 15:58:51 +020064 private AccessTokenDao tokenDao;
65 @Autowired
margarethaf370f542018-08-23 18:51:49 +020066 private RefreshTokenDao refreshDao;
67 @Autowired
margaretha34954472018-10-24 20:05:17 +020068 private AuthorizationDao authorizationDao;
margaretha7f5071f2018-08-14 15:58:51 +020069 @Autowired
margaretha8d804f62018-04-10 12:39:56 +020070 private AdminDao adminDao;
71 @Autowired
margarethae4034a82018-07-02 14:46:59 +020072 private UrlValidator redirectURIValidator;
margarethaa0486272018-04-12 19:59:31 +020073 @Autowired
margarethad7cab212018-07-02 19:01:43 +020074 private UrlValidator urlValidator;
75 @Autowired
margaretha31a9f522018-04-03 20:40:45 +020076 private EncryptionIface encryption;
margaretha0e8f4e72018-04-05 14:11:52 +020077 @Autowired
margaretha33fa3d92018-07-26 13:50:17 +020078 private RandomCodeGenerator codeGenerator;
79 @Autowired
margaretha6d61a552018-04-10 19:26:44 +020080 private FullConfiguration config;
margaretha31a9f522018-04-03 20:40:45 +020081
margaretha0e8f4e72018-04-05 14:11:52 +020082 public OAuth2ClientDto registerClient (OAuth2ClientJson clientJson,
83 String registeredBy) throws KustvaktException {
margaretha21d32962019-11-14 17:08:15 +010084
85 ParameterChecker.checkNameValue(clientJson.getName(), "clientName");
86
margarethad7cab212018-07-02 19:01:43 +020087 String url = clientJson.getUrl();
margarethad7cab212018-07-02 19:01:43 +020088 if (url != null && !url.isEmpty()) {
margaretha85273f12019-02-04 18:13:17 +010089 if (!urlValidator.isValid(url)) {
margarethad7cab212018-07-02 19:01:43 +020090 throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
91 url + " is invalid.", OAuth2Error.INVALID_REQUEST);
92 }
margaretha31a9f522018-04-03 20:40:45 +020093 }
margarethad7cab212018-07-02 19:01:43 +020094
95 String redirectURI = clientJson.getRedirectURI();
96 if (redirectURI != null && !redirectURI.isEmpty()
margaretha85273f12019-02-04 18:13:17 +010097 && !redirectURIValidator.isValid(redirectURI)) {
margarethad7cab212018-07-02 19:01:43 +020098 throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
99 redirectURI + " is invalid.", OAuth2Error.INVALID_REQUEST);
100 }
101
margaretha835178d2018-08-15 19:04:03 +0200102 // boolean isNative = isNativeClient(url, redirectURI);
margaretha6374f722018-04-17 18:45:57 +0200103
margaretha31a9f522018-04-03 20:40:45 +0200104 String secret = null;
margaretha6d61a552018-04-10 19:26:44 +0200105 String secretHashcode = null;
margaretha0e8f4e72018-04-05 14:11:52 +0200106 if (clientJson.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
margarethafb1e0992018-04-10 14:58:28 +0200107 // RFC 6749:
margaretha07402f42018-05-07 19:07:45 +0200108 // The authorization server MUST NOT issue client
109 // passwords or other client credentials to native
110 // application (clients installed and executed on the
111 // device used by the resource owner e.g. desktop
112 // application, native mobile application) or
113 // user-agent-based application clients for client
114 // authentication. The authorization server MAY issue a
115 // client password or other credentials for a specific
116 // installation of a native application client on a
margarethafb1e0992018-04-10 14:58:28 +0200117 // specific device.
118
margaretha33fa3d92018-07-26 13:50:17 +0200119 secret = codeGenerator.createRandomCode();
margaretha2618beb2020-01-24 14:12:28 +0100120 secretHashcode = encryption.secureHash(secret);
margaretha31a9f522018-04-03 20:40:45 +0200121 }
122
margaretha33fa3d92018-07-26 13:50:17 +0200123 String id = codeGenerator.createRandomCode();
margaretha8d804f62018-04-10 12:39:56 +0200124 try {
margaretha6d61a552018-04-10 19:26:44 +0200125 clientDao.registerClient(id, secretHashcode, clientJson.getName(),
Akrondc6d73d2020-04-15 16:40:04 +0200126 clientJson.getType(), url, redirectURI, registeredBy,
127 clientJson.getDescription());
margaretha8d804f62018-04-10 12:39:56 +0200128 }
129 catch (Exception e) {
130 Throwable cause = e;
131 Throwable lastCause = null;
132 while ((cause = cause.getCause()) != null
133 && !cause.equals(lastCause)) {
134 if (cause instanceof SQLException) {
margarethab1081b12018-07-03 23:35:01 +0200135 break;
margaretha8d804f62018-04-10 12:39:56 +0200136 }
137 lastCause = cause;
138 }
margarethab1081b12018-07-03 23:35:01 +0200139 throw new KustvaktException(StatusCodes.CLIENT_REGISTRATION_FAILED,
140 cause.getMessage(), OAuth2Error.INVALID_REQUEST);
margaretha8d804f62018-04-10 12:39:56 +0200141 }
margaretha0e8f4e72018-04-05 14:11:52 +0200142
143 return new OAuth2ClientDto(id, secret);
144 }
145
margaretha835178d2018-08-15 19:04:03 +0200146 @Deprecated
margaretha6374f722018-04-17 18:45:57 +0200147 private boolean isNativeClient (String url, String redirectURI)
148 throws KustvaktException {
margarethad7cab212018-07-02 19:01:43 +0200149 if (url == null || url.isEmpty() || redirectURI == null
150 || redirectURI.isEmpty()) {
151 return false;
152 }
153
margaretha6374f722018-04-17 18:45:57 +0200154 String nativeHost = config.getNativeClientHost();
155 String urlHost = null;
156 try {
157 urlHost = new URL(url).getHost();
158 }
159 catch (MalformedURLException e) {
160 throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
161 "Invalid url :" + e.getMessage(),
margarethaa452c5e2018-04-25 22:48:09 +0200162 OAuth2Error.INVALID_REQUEST);
margaretha6374f722018-04-17 18:45:57 +0200163 }
margarethad7cab212018-07-02 19:01:43 +0200164
165 if (!urlHost.equals(nativeHost)) {
166 return false;
167 }
168
margaretha6374f722018-04-17 18:45:57 +0200169 String uriHost = null;
170 try {
171 uriHost = new URI(redirectURI).getHost();
172 }
173 catch (URISyntaxException e) {
174 throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
margarethafb027f92018-04-23 20:00:13 +0200175 "Invalid redirectURI: " + e.getMessage(),
margarethaa452c5e2018-04-25 22:48:09 +0200176 OAuth2Error.INVALID_REQUEST);
margaretha6374f722018-04-17 18:45:57 +0200177 }
margarethad7cab212018-07-02 19:01:43 +0200178 if (!uriHost.equals(nativeHost)) {
179 return false;
180 }
181
182 return true;
margaretha6374f722018-04-17 18:45:57 +0200183 }
184
margaretha9a270a52020-02-05 16:31:14 +0100185 public void deregisterClient (String clientId, String username)
186 throws KustvaktException {
margaretha0e8f4e72018-04-05 14:11:52 +0200187
margarethafb027f92018-04-23 20:00:13 +0200188 OAuth2Client client = clientDao.retrieveClientById(clientId);
margaretha9a270a52020-02-05 16:31:14 +0100189
margaretha80ea0dd2018-07-03 14:22:59 +0200190 if (adminDao.isAdmin(username)
191 || client.getRegisteredBy().equals(username)) {
margaretha7f5071f2018-08-14 15:58:51 +0200192
margarethaf0085122018-08-16 16:19:53 +0200193 revokeAllAuthorizationsByClientId(clientId);
margaretha85273f12019-02-04 18:13:17 +0100194 clientDao.deregisterClient(client);
margaretha7f5071f2018-08-14 15:58:51 +0200195 }
196 else {
197 throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
198 "Unauthorized operation for user: " + username, username);
199 }
200 }
201
margarethaf0085122018-08-16 16:19:53 +0200202 private void revokeAllAuthorizationsByClientId (String clientId)
203 throws KustvaktException {
204
205 // revoke all related authorization codes
206 List<Authorization> authList =
207 authorizationDao.retrieveAuthorizationsByClientId(clientId);
208 for (Authorization authorization : authList) {
209 authorization.setRevoked(true);
210 authorizationDao.updateAuthorization(authorization);
211 }
212
213 // revoke all related access tokens
214 List<AccessToken> tokens =
margaretha0afd44a2020-02-05 10:49:21 +0100215 tokenDao.retrieveAccessTokenByClientId(clientId,null);
margarethaf0085122018-08-16 16:19:53 +0200216 for (AccessToken token : tokens) {
217 token.setRevoked(true);
margarethaf0085122018-08-16 16:19:53 +0200218 tokenDao.updateAccessToken(token);
219 }
margarethaf370f542018-08-23 18:51:49 +0200220
221 List<RefreshToken> refreshTokens =
margaretha0afd44a2020-02-05 10:49:21 +0100222 refreshDao.retrieveRefreshTokenByClientId(clientId,null);
margarethaf370f542018-08-23 18:51:49 +0200223 for (RefreshToken token : refreshTokens) {
224 token.setRevoked(true);
225 refreshDao.updateRefreshToken(token);
226 }
margarethaf0085122018-08-16 16:19:53 +0200227 }
228
margaretha9a270a52020-02-05 16:31:14 +0100229 public OAuth2ClientDto resetSecret (String clientId, String username)
230 throws KustvaktException {
margaretha7f5071f2018-08-14 15:58:51 +0200231
margaretha9a270a52020-02-05 16:31:14 +0100232 OAuth2Client client = clientDao.retrieveClientById(clientId);
margaretha7f5071f2018-08-14 15:58:51 +0200233 if (!client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
margaretha835178d2018-08-15 19:04:03 +0200234 throw new KustvaktException(StatusCodes.NOT_ALLOWED,
margaretha7f5071f2018-08-14 15:58:51 +0200235 "Operation is not allowed for public clients",
236 OAuth2Error.INVALID_REQUEST);
237 }
238 if (adminDao.isAdmin(username)
239 || client.getRegisteredBy().equals(username)) {
240
241 String secret = codeGenerator.createRandomCode();
margaretha2618beb2020-01-24 14:12:28 +0100242 String secretHashcode = encryption.secureHash(secret);
margaretha7f5071f2018-08-14 15:58:51 +0200243
244 client.setSecret(secretHashcode);
245 clientDao.updateClient(client);
246 return new OAuth2ClientDto(clientId, secret);
margaretha8d804f62018-04-10 12:39:56 +0200247 }
248 else {
249 throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
250 "Unauthorized operation for user: " + username, username);
251 }
margaretha0e8f4e72018-04-05 14:11:52 +0200252 }
253
margarethafb027f92018-04-23 20:00:13 +0200254 public OAuth2Client authenticateClient (String clientId,
255 String clientSecret) throws KustvaktException {
margaretha6374f722018-04-17 18:45:57 +0200256
margarethafb027f92018-04-23 20:00:13 +0200257 if (clientId == null || clientId.isEmpty()) {
margaretha05122312018-04-16 15:01:34 +0200258 throw new KustvaktException(
259 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
margarethaa452c5e2018-04-25 22:48:09 +0200260 "Missing parameters: client id",
261 OAuth2Error.INVALID_REQUEST);
margarethaa0486272018-04-12 19:59:31 +0200262 }
margaretha0e8f4e72018-04-05 14:11:52 +0200263
margarethafb027f92018-04-23 20:00:13 +0200264 OAuth2Client client = clientDao.retrieveClientById(clientId);
margaretha80ea0dd2018-07-03 14:22:59 +0200265 authenticateClient(client, clientSecret);
266 return client;
267 }
268
269 public void authenticateClient (OAuth2Client client, String clientSecret)
270 throws KustvaktException {
margaretha7f5071f2018-08-14 15:58:51 +0200271 if (clientSecret == null || clientSecret.isEmpty()) {
margaretha80ea0dd2018-07-03 14:22:59 +0200272 if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
273 throw new KustvaktException(
274 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
275 "Missing parameters: client_secret",
276 OAuth2Error.INVALID_REQUEST);
margaretha0e8f4e72018-04-05 14:11:52 +0200277 }
278 }
margarethaf0085122018-08-16 16:19:53 +0200279 else if (client.getSecret() == null || client.getSecret().isEmpty()) {
280 if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
margaretha835178d2018-08-15 19:04:03 +0200281 throw new KustvaktException(
282 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
283 "Client secret was not registered",
284 OAuth2Error.INVALID_CLIENT);
285 }
286 }
margaretha2618beb2020-01-24 14:12:28 +0100287 else if (!encryption.checkHash(clientSecret, client.getSecret())) {
margaretha80ea0dd2018-07-03 14:22:59 +0200288 throw new KustvaktException(
289 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
290 "Invalid client credentials", OAuth2Error.INVALID_CLIENT);
291 }
margaretha31a9f522018-04-03 20:40:45 +0200292 }
margarethafb1e0992018-04-10 14:58:28 +0200293
margaretha07402f42018-05-07 19:07:45 +0200294 public OAuth2Client authenticateClientId (String clientId)
295 throws KustvaktException {
296 if (clientId == null || clientId.isEmpty()) {
297 throw new KustvaktException(
298 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
299 "Missing parameters: client id",
300 OAuth2Error.INVALID_REQUEST);
301 }
302
303 return clientDao.retrieveClientById(clientId);
304 }
margaretha835178d2018-08-15 19:04:03 +0200305
306 public void updatePrivilege (String username, String clientId,
307 boolean isSuper) throws KustvaktException {
308
309 if (adminDao.isAdmin(username)) {
310 OAuth2Client client = clientDao.retrieveClientById(clientId);
margarethaf0085122018-08-16 16:19:53 +0200311 if (isSuper) {
312 if (!client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
313 throw new KustvaktException(StatusCodes.NOT_ALLOWED,
314 "Only confidential clients are allowed to be super clients.");
315 }
margaretha835178d2018-08-15 19:04:03 +0200316 }
margarethaf0085122018-08-16 16:19:53 +0200317 else {
318 revokeAllAuthorizationsByClientId(clientId);
319 }
320
margaretha835178d2018-08-15 19:04:03 +0200321 client.setSuper(isSuper);
322 clientDao.updateClient(client);
323 }
324 else {
325 throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
326 "Unauthorized operation for user: " + username, username);
327 }
328 }
329
330 public OAuth2ClientInfoDto retrieveClientInfo (String username,
331 String clientId) throws KustvaktException {
332 OAuth2Client client = clientDao.retrieveClientById(clientId);
333 if (adminDao.isAdmin(username)
334 || username.equals(client.getRegisteredBy())) {
335 return new OAuth2ClientInfoDto(client);
336 }
337 else {
338 throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
339 "Unauthorized operation for user: " + username, username);
340 }
341 }
margaretha230effb2018-11-29 17:28:18 +0100342
343 public OAuth2Client retrieveClient (String clientId)
344 throws KustvaktException {
345 return clientDao.retrieveClientById(clientId);
346 }
347
margaretha7a09e482019-11-14 14:34:07 +0100348 public List<OAuth2UserClientDto> listUserAuthorizedClients (String username,
margaretha230effb2018-11-29 17:28:18 +0100349 String clientId, String clientSecret) throws KustvaktException {
350 OAuth2Client client = authenticateClient(clientId, clientSecret);
351 if (!client.isSuper()) {
352 throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
margaretha7a09e482019-11-14 14:34:07 +0100353 "Only super client is allowed to list user authorized clients.",
margaretha230effb2018-11-29 17:28:18 +0100354 OAuth2Error.UNAUTHORIZED_CLIENT);
355 }
margaretha0afd44a2020-02-05 10:49:21 +0100356
margaretha5a2c34e2018-11-29 19:35:13 +0100357 List<OAuth2Client> userClients =
margaretha7a09e482019-11-14 14:34:07 +0100358 clientDao.retrieveUserAuthorizedClients(username);
margaretha0afd44a2020-02-05 10:49:21 +0100359 userClients.addAll(clientDao.retrieveClientsByAccessTokens(username));
360
361 List<String> clientIds = new ArrayList<>();
362 List<OAuth2Client> uniqueClients = new ArrayList<>();
363 for (OAuth2Client c : userClients){
364 String id = c.getId();
365 if (!clientIds.contains(id)){
366 clientIds.add(id);
367 uniqueClients.add(c);
368 }
369 }
370
371 Collections.sort(uniqueClients);
372 return createClientDtos(uniqueClients);
margaretha7a09e482019-11-14 14:34:07 +0100373 }
374
375 public List<OAuth2UserClientDto> listUserRegisteredClients (String username,
376 String clientId, String clientSecret) throws KustvaktException {
377 OAuth2Client client = authenticateClient(clientId, clientSecret);
378 if (!client.isSuper()) {
379 throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
380 "Only super client is allowed to list user registered clients.",
381 OAuth2Error.UNAUTHORIZED_CLIENT);
382 }
383 List<OAuth2Client> userClients =
384 clientDao.retrieveUserRegisteredClients(username);
385 Collections.sort(userClients);
386 return createClientDtos(userClients);
387 }
388
389 private List<OAuth2UserClientDto> createClientDtos (List<OAuth2Client> userClients) {
margaretha230effb2018-11-29 17:28:18 +0100390 List<OAuth2UserClientDto> dtoList = new ArrayList<>(userClients.size());
391 for (OAuth2Client uc : userClients) {
392 if (uc.isSuper()) continue;
393 OAuth2UserClientDto dto = new OAuth2UserClientDto();
394 dto.setClientId(uc.getId());
395 dto.setClientName(uc.getName());
margarethac20cd342019-11-14 10:59:39 +0100396 dto.setDescription(uc.getDescription());
397 dto.setUrl(uc.getUrl());
margaretha230effb2018-11-29 17:28:18 +0100398 dtoList.add(dto);
399 }
400 return dtoList;
401 }
margaretha0afd44a2020-02-05 10:49:21 +0100402
403 public boolean isPublicClient (OAuth2Client oAuth2Client) {
404 return oAuth2Client.getType().equals(OAuth2ClientType.PUBLIC);
405 }
margaretha31a9f522018-04-03 20:40:45 +0200406}