blob: fb7e5eaf2ebfc020d192e5f82928ead015febd43 [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;
margaretha31a9f522018-04-03 20:40:45 +020035import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
36
margarethaa452c5e2018-04-25 22:48:09 +020037/**
margaretha398f4722019-01-09 19:07:20 +010038 * Defines business logic related to OAuth2 client including
39 * client registration and client authentication.
40 *
margarethaa452c5e2018-04-25 22:48:09 +020041 * According to RFC 6749, an authorization server MUST:
margarethaa0486272018-04-12 19:59:31 +020042 * <ul>
43 * <li>
44 * require client authentication for confidential clients or for any
margarethaa452c5e2018-04-25 22:48:09 +020045 * client that was issued client credentials (or with other
46 * authentication
margarethaa0486272018-04-12 19:59:31 +020047 * requirements),
48 * </li>
49 *
50 * <li>authenticate the client if client authentication is included
51 * </li>
52 * </ul>
53 *
54 * @author margaretha
55 *
56 */
margaretha31a9f522018-04-03 20:40:45 +020057@Service
58public class OAuth2ClientService {
59
60 @Autowired
61 private OAuth2ClientDao clientDao;
62 @Autowired
margaretha7f5071f2018-08-14 15:58:51 +020063 private AccessTokenDao tokenDao;
64 @Autowired
margarethaf370f542018-08-23 18:51:49 +020065 private RefreshTokenDao refreshDao;
66 @Autowired
margaretha34954472018-10-24 20:05:17 +020067 private AuthorizationDao authorizationDao;
margaretha7f5071f2018-08-14 15:58:51 +020068 @Autowired
margaretha8d804f62018-04-10 12:39:56 +020069 private AdminDao adminDao;
70 @Autowired
margarethae4034a82018-07-02 14:46:59 +020071 private UrlValidator redirectURIValidator;
margarethaa0486272018-04-12 19:59:31 +020072 @Autowired
margarethad7cab212018-07-02 19:01:43 +020073 private UrlValidator urlValidator;
74 @Autowired
margaretha31a9f522018-04-03 20:40:45 +020075 private EncryptionIface encryption;
margaretha0e8f4e72018-04-05 14:11:52 +020076 @Autowired
margaretha33fa3d92018-07-26 13:50:17 +020077 private RandomCodeGenerator codeGenerator;
78 @Autowired
margaretha6d61a552018-04-10 19:26:44 +020079 private FullConfiguration config;
margaretha31a9f522018-04-03 20:40:45 +020080
margaretha0e8f4e72018-04-05 14:11:52 +020081 public OAuth2ClientDto registerClient (OAuth2ClientJson clientJson,
82 String registeredBy) throws KustvaktException {
margarethad7cab212018-07-02 19:01:43 +020083 String url = clientJson.getUrl();
84 int urlHashCode = 0;
85 if (url != null && !url.isEmpty()) {
86 urlHashCode = clientJson.getUrl().hashCode();
margaretha85273f12019-02-04 18:13:17 +010087 if (!urlValidator.isValid(url)) {
margarethad7cab212018-07-02 19:01:43 +020088 throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
89 url + " is invalid.", OAuth2Error.INVALID_REQUEST);
90 }
margaretha31a9f522018-04-03 20:40:45 +020091 }
margarethad7cab212018-07-02 19:01:43 +020092
93 String redirectURI = clientJson.getRedirectURI();
94 if (redirectURI != null && !redirectURI.isEmpty()
margaretha85273f12019-02-04 18:13:17 +010095 && !redirectURIValidator.isValid(redirectURI)) {
margarethad7cab212018-07-02 19:01:43 +020096 throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
97 redirectURI + " is invalid.", OAuth2Error.INVALID_REQUEST);
98 }
99
margaretha835178d2018-08-15 19:04:03 +0200100 // boolean isNative = isNativeClient(url, redirectURI);
margaretha6374f722018-04-17 18:45:57 +0200101
margaretha31a9f522018-04-03 20:40:45 +0200102 String secret = null;
margaretha6d61a552018-04-10 19:26:44 +0200103 String secretHashcode = null;
margaretha0e8f4e72018-04-05 14:11:52 +0200104 if (clientJson.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
margarethafb1e0992018-04-10 14:58:28 +0200105 // RFC 6749:
margaretha07402f42018-05-07 19:07:45 +0200106 // The authorization server MUST NOT issue client
107 // passwords or other client credentials to native
108 // application (clients installed and executed on the
109 // device used by the resource owner e.g. desktop
110 // application, native mobile application) or
111 // user-agent-based application clients for client
112 // authentication. The authorization server MAY issue a
113 // client password or other credentials for a specific
114 // installation of a native application client on a
margarethafb1e0992018-04-10 14:58:28 +0200115 // specific device.
116
margaretha33fa3d92018-07-26 13:50:17 +0200117 secret = codeGenerator.createRandomCode();
margaretha6d61a552018-04-10 19:26:44 +0200118 secretHashcode = encryption.secureHash(secret,
119 config.getPasscodeSaltField());
margaretha31a9f522018-04-03 20:40:45 +0200120 }
121
margaretha33fa3d92018-07-26 13:50:17 +0200122 String id = codeGenerator.createRandomCode();
margaretha8d804f62018-04-10 12:39:56 +0200123 try {
margaretha6d61a552018-04-10 19:26:44 +0200124 clientDao.registerClient(id, secretHashcode, clientJson.getName(),
margaretha835178d2018-08-15 19:04:03 +0200125 clientJson.getType(), url, urlHashCode, redirectURI,
126 registeredBy, clientJson.getDescription());
margaretha8d804f62018-04-10 12:39:56 +0200127 }
128 catch (Exception e) {
129 Throwable cause = e;
130 Throwable lastCause = null;
131 while ((cause = cause.getCause()) != null
132 && !cause.equals(lastCause)) {
133 if (cause instanceof SQLException) {
margarethab1081b12018-07-03 23:35:01 +0200134 break;
margaretha8d804f62018-04-10 12:39:56 +0200135 }
136 lastCause = cause;
137 }
margarethab1081b12018-07-03 23:35:01 +0200138 throw new KustvaktException(StatusCodes.CLIENT_REGISTRATION_FAILED,
139 cause.getMessage(), OAuth2Error.INVALID_REQUEST);
margaretha8d804f62018-04-10 12:39:56 +0200140 }
margaretha0e8f4e72018-04-05 14:11:52 +0200141
142 return new OAuth2ClientDto(id, secret);
143 }
144
margaretha835178d2018-08-15 19:04:03 +0200145 @Deprecated
margaretha6374f722018-04-17 18:45:57 +0200146 private boolean isNativeClient (String url, String redirectURI)
147 throws KustvaktException {
margarethad7cab212018-07-02 19:01:43 +0200148 if (url == null || url.isEmpty() || redirectURI == null
149 || redirectURI.isEmpty()) {
150 return false;
151 }
152
margaretha6374f722018-04-17 18:45:57 +0200153 String nativeHost = config.getNativeClientHost();
154 String urlHost = null;
155 try {
156 urlHost = new URL(url).getHost();
157 }
158 catch (MalformedURLException e) {
159 throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
160 "Invalid url :" + e.getMessage(),
margarethaa452c5e2018-04-25 22:48:09 +0200161 OAuth2Error.INVALID_REQUEST);
margaretha6374f722018-04-17 18:45:57 +0200162 }
margarethad7cab212018-07-02 19:01:43 +0200163
164 if (!urlHost.equals(nativeHost)) {
165 return false;
166 }
167
margaretha6374f722018-04-17 18:45:57 +0200168 String uriHost = null;
169 try {
170 uriHost = new URI(redirectURI).getHost();
171 }
172 catch (URISyntaxException e) {
173 throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
margarethafb027f92018-04-23 20:00:13 +0200174 "Invalid redirectURI: " + e.getMessage(),
margarethaa452c5e2018-04-25 22:48:09 +0200175 OAuth2Error.INVALID_REQUEST);
margaretha6374f722018-04-17 18:45:57 +0200176 }
margarethad7cab212018-07-02 19:01:43 +0200177 if (!uriHost.equals(nativeHost)) {
178 return false;
179 }
180
181 return true;
margaretha6374f722018-04-17 18:45:57 +0200182 }
183
margaretha80ea0dd2018-07-03 14:22:59 +0200184 public void deregisterClient (String clientId, String clientSecret,
185 String username) throws KustvaktException {
margaretha0e8f4e72018-04-05 14:11:52 +0200186
margarethafb027f92018-04-23 20:00:13 +0200187 OAuth2Client client = clientDao.retrieveClientById(clientId);
margaretha80ea0dd2018-07-03 14:22:59 +0200188 if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
189 authenticateClient(clientId, clientSecret);
margaretha8d804f62018-04-10 12:39:56 +0200190 }
margaretha80ea0dd2018-07-03 14:22:59 +0200191
192 if (adminDao.isAdmin(username)
193 || client.getRegisteredBy().equals(username)) {
margaretha7f5071f2018-08-14 15:58:51 +0200194
margarethaf0085122018-08-16 16:19:53 +0200195 revokeAllAuthorizationsByClientId(clientId);
margaretha85273f12019-02-04 18:13:17 +0100196 clientDao.deregisterClient(client);
margaretha7f5071f2018-08-14 15:58:51 +0200197 }
198 else {
199 throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
200 "Unauthorized operation for user: " + username, username);
201 }
202 }
203
margarethaf0085122018-08-16 16:19:53 +0200204 private void revokeAllAuthorizationsByClientId (String clientId)
205 throws KustvaktException {
206
207 // revoke all related authorization codes
208 List<Authorization> authList =
209 authorizationDao.retrieveAuthorizationsByClientId(clientId);
210 for (Authorization authorization : authList) {
211 authorization.setRevoked(true);
212 authorizationDao.updateAuthorization(authorization);
213 }
214
215 // revoke all related access tokens
216 List<AccessToken> tokens =
217 tokenDao.retrieveAccessTokenByClientId(clientId);
218 for (AccessToken token : tokens) {
219 token.setRevoked(true);
margarethaf0085122018-08-16 16:19:53 +0200220 tokenDao.updateAccessToken(token);
221 }
margarethaf370f542018-08-23 18:51:49 +0200222
223 List<RefreshToken> refreshTokens =
224 refreshDao.retrieveRefreshTokenByClientId(clientId);
225 for (RefreshToken token : refreshTokens) {
226 token.setRevoked(true);
227 refreshDao.updateRefreshToken(token);
228 }
margarethaf0085122018-08-16 16:19:53 +0200229 }
230
margaretha7f5071f2018-08-14 15:58:51 +0200231 public OAuth2ClientDto resetSecret (String clientId, String clientSecret,
232 String username) throws KustvaktException {
233
234 OAuth2Client client = authenticateClient(clientId, clientSecret);
235 if (!client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
margaretha835178d2018-08-15 19:04:03 +0200236 throw new KustvaktException(StatusCodes.NOT_ALLOWED,
margaretha7f5071f2018-08-14 15:58:51 +0200237 "Operation is not allowed for public clients",
238 OAuth2Error.INVALID_REQUEST);
239 }
240 if (adminDao.isAdmin(username)
241 || client.getRegisteredBy().equals(username)) {
242
243 String secret = codeGenerator.createRandomCode();
244 String secretHashcode = encryption.secureHash(secret,
245 config.getPasscodeSaltField());
246
247 client.setSecret(secretHashcode);
248 clientDao.updateClient(client);
249 return new OAuth2ClientDto(clientId, secret);
margaretha8d804f62018-04-10 12:39:56 +0200250 }
251 else {
252 throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
253 "Unauthorized operation for user: " + username, username);
254 }
margaretha0e8f4e72018-04-05 14:11:52 +0200255 }
256
margarethafb027f92018-04-23 20:00:13 +0200257 public OAuth2Client authenticateClient (String clientId,
258 String clientSecret) throws KustvaktException {
margaretha6374f722018-04-17 18:45:57 +0200259
margarethafb027f92018-04-23 20:00:13 +0200260 if (clientId == null || clientId.isEmpty()) {
margaretha05122312018-04-16 15:01:34 +0200261 throw new KustvaktException(
262 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
margarethaa452c5e2018-04-25 22:48:09 +0200263 "Missing parameters: client id",
264 OAuth2Error.INVALID_REQUEST);
margarethaa0486272018-04-12 19:59:31 +0200265 }
margaretha0e8f4e72018-04-05 14:11:52 +0200266
margarethafb027f92018-04-23 20:00:13 +0200267 OAuth2Client client = clientDao.retrieveClientById(clientId);
margaretha80ea0dd2018-07-03 14:22:59 +0200268 authenticateClient(client, clientSecret);
269 return client;
270 }
271
272 public void authenticateClient (OAuth2Client client, String clientSecret)
273 throws KustvaktException {
margaretha7f5071f2018-08-14 15:58:51 +0200274 if (clientSecret == null || clientSecret.isEmpty()) {
margaretha80ea0dd2018-07-03 14:22:59 +0200275 if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
276 throw new KustvaktException(
277 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
278 "Missing parameters: client_secret",
279 OAuth2Error.INVALID_REQUEST);
margaretha0e8f4e72018-04-05 14:11:52 +0200280 }
281 }
margarethaf0085122018-08-16 16:19:53 +0200282 else if (client.getSecret() == null || client.getSecret().isEmpty()) {
283 if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
margaretha835178d2018-08-15 19:04:03 +0200284 throw new KustvaktException(
285 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
286 "Client secret was not registered",
287 OAuth2Error.INVALID_CLIENT);
288 }
289 }
margaretha80ea0dd2018-07-03 14:22:59 +0200290 else if (!encryption.checkHash(clientSecret, client.getSecret(),
291 config.getPasscodeSaltField())) {
292 throw new KustvaktException(
293 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
294 "Invalid client credentials", OAuth2Error.INVALID_CLIENT);
295 }
margaretha31a9f522018-04-03 20:40:45 +0200296 }
margarethafb1e0992018-04-10 14:58:28 +0200297
margaretha07402f42018-05-07 19:07:45 +0200298 public OAuth2Client authenticateClientId (String clientId)
299 throws KustvaktException {
300 if (clientId == null || clientId.isEmpty()) {
301 throw new KustvaktException(
302 StatusCodes.CLIENT_AUTHENTICATION_FAILED,
303 "Missing parameters: client id",
304 OAuth2Error.INVALID_REQUEST);
305 }
306
307 return clientDao.retrieveClientById(clientId);
308 }
margaretha835178d2018-08-15 19:04:03 +0200309
310 public void updatePrivilege (String username, String clientId,
311 boolean isSuper) throws KustvaktException {
312
313 if (adminDao.isAdmin(username)) {
314 OAuth2Client client = clientDao.retrieveClientById(clientId);
margarethaf0085122018-08-16 16:19:53 +0200315 if (isSuper) {
316 if (!client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
317 throw new KustvaktException(StatusCodes.NOT_ALLOWED,
318 "Only confidential clients are allowed to be super clients.");
319 }
margaretha835178d2018-08-15 19:04:03 +0200320 }
margarethaf0085122018-08-16 16:19:53 +0200321 else {
322 revokeAllAuthorizationsByClientId(clientId);
323 }
324
margaretha835178d2018-08-15 19:04:03 +0200325 client.setSuper(isSuper);
326 clientDao.updateClient(client);
327 }
328 else {
329 throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
330 "Unauthorized operation for user: " + username, username);
331 }
332 }
333
334 public OAuth2ClientInfoDto retrieveClientInfo (String username,
335 String clientId) throws KustvaktException {
336 OAuth2Client client = clientDao.retrieveClientById(clientId);
337 if (adminDao.isAdmin(username)
338 || username.equals(client.getRegisteredBy())) {
339 return new OAuth2ClientInfoDto(client);
340 }
341 else {
342 throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
343 "Unauthorized operation for user: " + username, username);
344 }
345 }
margaretha230effb2018-11-29 17:28:18 +0100346
347 public OAuth2Client retrieveClient (String clientId)
348 throws KustvaktException {
349 return clientDao.retrieveClientById(clientId);
350 }
351
margaretha230effb2018-11-29 17:28:18 +0100352 public List<OAuth2UserClientDto> listUserClients (String username,
353 String clientId, String clientSecret) throws KustvaktException {
354 OAuth2Client client = authenticateClient(clientId, clientSecret);
355 if (!client.isSuper()) {
356 throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
357 "Only super client is allowed to list user clients.",
358 OAuth2Error.UNAUTHORIZED_CLIENT);
359 }
margaretha5a2c34e2018-11-29 19:35:13 +0100360 List<OAuth2Client> userClients =
361 clientDao.retrieveUserClients(username);
margaretha230effb2018-11-29 17:28:18 +0100362 Collections.sort(userClients);
margaretha5a2c34e2018-11-29 19:35:13 +0100363
margaretha230effb2018-11-29 17:28:18 +0100364 List<OAuth2UserClientDto> dtoList = new ArrayList<>(userClients.size());
365 for (OAuth2Client uc : userClients) {
366 if (uc.isSuper()) continue;
367 OAuth2UserClientDto dto = new OAuth2UserClientDto();
368 dto.setClientId(uc.getId());
369 dto.setClientName(uc.getName());
370 dtoList.add(dto);
371 }
372 return dtoList;
373 }
margaretha31a9f522018-04-03 20:40:45 +0200374}