|
17 | 17 | *******************************************************************************/
|
18 | 18 | package cz.muni.ics.oauth2.web.endpoint;
|
19 | 19 |
|
20 |
| -import com.google.common.base.Strings; |
21 | 20 | import com.google.common.collect.ImmutableMap;
|
22 | 21 | import cz.muni.ics.oauth2.model.ClientDetailsEntity;
|
23 | 22 | import cz.muni.ics.oauth2.model.OAuth2AccessTokenEntity;
|
24 | 23 | import cz.muni.ics.oauth2.model.OAuth2RefreshTokenEntity;
|
25 | 24 | import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
|
26 | 25 | import cz.muni.ics.oauth2.service.IntrospectionResultAssembler;
|
27 | 26 | import cz.muni.ics.oauth2.service.OAuth2TokenEntityService;
|
28 |
| -import cz.muni.ics.oauth2.service.SystemScopeService; |
29 | 27 | import cz.muni.ics.oauth2.web.AuthenticationUtilities;
|
30 | 28 | import cz.muni.ics.openid.connect.model.UserInfo;
|
31 | 29 | import cz.muni.ics.openid.connect.service.UserInfoService;
|
32 | 30 | import cz.muni.ics.openid.connect.view.HttpCodeView;
|
33 | 31 | import cz.muni.ics.openid.connect.view.JsonEntityView;
|
34 |
| -import java.util.HashSet; |
| 32 | + |
| 33 | +import java.util.HashMap; |
35 | 34 | import java.util.Map;
|
36 | 35 | import java.util.Set;
|
37 | 36 | import lombok.extern.slf4j.Slf4j;
|
38 | 37 | import org.springframework.beans.factory.annotation.Autowired;
|
39 | 38 | import org.springframework.http.HttpStatus;
|
40 | 39 | import org.springframework.security.core.Authentication;
|
41 | 40 | import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
|
42 |
| -import org.springframework.security.oauth2.provider.OAuth2Authentication; |
43 | 41 | import org.springframework.stereotype.Controller;
|
44 | 42 | import org.springframework.ui.Model;
|
| 43 | +import org.springframework.util.StringUtils; |
45 | 44 | import org.springframework.web.bind.annotation.RequestMapping;
|
46 | 45 | import org.springframework.web.bind.annotation.RequestParam;
|
47 | 46 |
|
48 | 47 | @Controller
|
49 | 48 | @Slf4j
|
50 | 49 | public class IntrospectionEndpoint {
|
51 | 50 |
|
52 |
| - /** |
53 |
| - * |
54 |
| - */ |
55 | 51 | public static final String URL = "introspect";
|
56 | 52 |
|
57 |
| - @Autowired |
58 |
| - private OAuth2TokenEntityService tokenServices; |
59 |
| - |
60 |
| - @Autowired |
61 |
| - private ClientDetailsEntityService clientService; |
| 53 | + public static final String PARAM_TOKEN = "token"; |
| 54 | + public static final String PARAM_TOKEN_TYPE_HINT = "token_type_hint"; |
62 | 55 |
|
63 |
| - @Autowired |
64 |
| - private IntrospectionResultAssembler introspectionResultAssembler; |
| 56 | + private final OAuth2TokenEntityService tokenServices; |
| 57 | + private final ClientDetailsEntityService clientService; |
| 58 | + private final IntrospectionResultAssembler introspectionResultAssembler; |
| 59 | + private final UserInfoService userInfoService; |
65 | 60 |
|
66 | 61 | @Autowired
|
67 |
| - private UserInfoService userInfoService; |
68 |
| - |
69 |
| - public IntrospectionEndpoint() { |
70 |
| - |
71 |
| - } |
72 |
| - |
73 |
| - public IntrospectionEndpoint(OAuth2TokenEntityService tokenServices) { |
| 62 | + public IntrospectionEndpoint(OAuth2TokenEntityService tokenServices, |
| 63 | + ClientDetailsEntityService clientService, |
| 64 | + IntrospectionResultAssembler introspectionResultAssembler, |
| 65 | + UserInfoService userInfoService) |
| 66 | + { |
74 | 67 | this.tokenServices = tokenServices;
|
| 68 | + this.clientService = clientService; |
| 69 | + this.introspectionResultAssembler = introspectionResultAssembler; |
| 70 | + this.userInfoService = userInfoService; |
75 | 71 | }
|
76 | 72 |
|
77 | 73 | @RequestMapping("/" + URL)
|
78 |
| - public String verify(@RequestParam("token") String tokenValue, |
79 |
| - @RequestParam(value = "token_type_hint", required = false) String tokenType, |
80 |
| - Authentication auth, Model model) { |
81 |
| - |
82 |
| - ClientDetailsEntity authClient = null; |
83 |
| - Set<String> authScopes = new HashSet<>(); |
| 74 | + public String introspect(@RequestParam(PARAM_TOKEN) String token, |
| 75 | + @RequestParam(value = PARAM_TOKEN_TYPE_HINT, required = false) String tokenTypeHint, |
| 76 | + Authentication auth, |
| 77 | + Model model) |
| 78 | + { |
| 79 | + if (auth == null) { |
| 80 | + log.error("No authentication object available in the introspection endpoint"); |
| 81 | + return codeErrorResponse(model, HttpStatus.UNAUTHORIZED); |
| 82 | + } |
84 | 83 |
|
85 |
| - if (auth instanceof OAuth2Authentication) { |
86 |
| - // the client authenticated with OAuth, do our UMA checks |
87 |
| - AuthenticationUtilities.ensureOAuthScope(auth, SystemScopeService.UMA_PROTECTION_SCOPE); |
| 84 | + String authClientId = auth.getName(); |
| 85 | + if (!StringUtils.hasText(authClientId)) { |
| 86 | + log.error("No client_id object available in the introspection endpoint"); |
| 87 | + return codeErrorResponse(model, HttpStatus.INTERNAL_SERVER_ERROR); |
| 88 | + } |
88 | 89 |
|
89 |
| - // get out the client that was issued the access token (not the token being introspected) |
90 |
| - OAuth2Authentication o2a = (OAuth2Authentication) auth; |
| 90 | + ClientDetailsEntity authClient = clientService.loadClientByClientId(authClientId); |
| 91 | + if (authClient == null) { |
| 92 | + log.error("No client found for client_id '{}'", authClientId); |
| 93 | + return codeErrorResponse(model, HttpStatus.BAD_REQUEST); |
| 94 | + } else if (!AuthenticationUtilities.hasRole(auth, "ROLE_CLIENT") || !authClient.isAllowIntrospection()) { |
| 95 | + log.error("Client '{}' is not allowed to call introspection endpoint", authClient.getClientId()); |
| 96 | + return codeErrorResponse(model, HttpStatus.FORBIDDEN); |
| 97 | + } |
91 | 98 |
|
92 |
| - String authClientId = o2a.getOAuth2Request().getClientId(); |
93 |
| - authClient = clientService.loadClientByClientId(authClientId); |
| 99 | + return introspectToken(model, token, tokenTypeHint, authClient); |
| 100 | + } |
94 | 101 |
|
95 |
| - // the owner is the user who authorized the token in the first place |
96 |
| - String ownerId = o2a.getUserAuthentication().getName(); |
| 102 | + private String introspectToken(Model model, String token, String tokenTypeHint, ClientDetailsEntity authClient) { |
| 103 | + Map<String, Object> entity; |
| 104 | + if (!StringUtils.hasText(token)) { |
| 105 | + log.error("Token introspection failed; token ('{}') not provided", token); |
| 106 | + entity = introspectUnknownToken(); |
| 107 | + return jsonResponse(model, entity); |
| 108 | + } |
97 | 109 |
|
98 |
| - authScopes.addAll(authClient.getScope()); |
| 110 | + if ("refresh_token".equals(tokenTypeHint)) { |
| 111 | + entity = introspectRefreshToken(token, authClient.getScope()); |
| 112 | + if (entity != null) { |
| 113 | + return jsonResponse(model, entity); |
| 114 | + } else { |
| 115 | + entity = introspectAccessToken(token, authClient.getScope()); |
| 116 | + } |
| 117 | + } else if (tokenTypeHint.equals("access_token")) { |
| 118 | + entity = introspectAccessToken(token, authClient.getScope()); |
| 119 | + if (entity != null) { |
| 120 | + return jsonResponse(model, entity); |
| 121 | + } else { |
| 122 | + entity = introspectRefreshToken(token, authClient.getScope()); |
| 123 | + } |
99 | 124 | } else {
|
100 |
| - // the client authenticated directly, make sure it's got the right access |
101 |
| - |
102 |
| - String authClientId = auth.getName(); // direct authentication puts the client_id into the authentication's name field |
103 |
| - authClient = clientService.loadClientByClientId(authClientId); |
104 |
| - |
105 |
| - // directly authenticated clients get a subset of any scopes that they've registered for |
106 |
| - authScopes.addAll(authClient.getScope()); |
107 |
| - |
108 |
| - if (!AuthenticationUtilities.hasRole(auth, "ROLE_CLIENT") |
109 |
| - || !authClient.isAllowIntrospection()) { |
110 |
| - |
111 |
| - // this client isn't allowed to do direct introspection |
112 |
| - |
113 |
| - log.error("Client " + authClient.getClientId() + " is not allowed to call introspection endpoint"); |
114 |
| - model.addAttribute("code", HttpStatus.FORBIDDEN); |
115 |
| - return HttpCodeView.VIEWNAME; |
116 |
| - |
| 125 | + entity = introspectAccessToken(token, authClient.getScope()); |
| 126 | + if (entity != null) { |
| 127 | + return jsonResponse(model, entity); |
| 128 | + } else { |
| 129 | + entity = introspectRefreshToken(token, authClient.getScope()); |
117 | 130 | }
|
118 |
| - |
119 | 131 | }
|
120 | 132 |
|
121 |
| - // by here we're allowed to introspect, now we need to look up the token in our token stores |
122 |
| - |
123 |
| - // first make sure the token is there |
124 |
| - if (Strings.isNullOrEmpty(tokenValue)) { |
125 |
| - log.error("Verify failed; token value is null"); |
126 |
| - Map<String,Boolean> entity = ImmutableMap.of("active", Boolean.FALSE); |
127 |
| - model.addAttribute(JsonEntityView.ENTITY, entity); |
128 |
| - return JsonEntityView.VIEWNAME; |
| 133 | + if (entity == null) { |
| 134 | + entity = introspectUnknownToken(); |
129 | 135 | }
|
| 136 | + return jsonResponse(model, entity); |
| 137 | + } |
130 | 138 |
|
131 |
| - OAuth2AccessTokenEntity accessToken = null; |
132 |
| - OAuth2RefreshTokenEntity refreshToken = null; |
133 |
| - ClientDetailsEntity tokenClient; |
134 |
| - UserInfo user; |
| 139 | + private Map<String, Object> introspectUnknownToken() { |
| 140 | + return ImmutableMap.of(IntrospectionResultAssembler.ACTIVE, false); |
| 141 | + } |
135 | 142 |
|
| 143 | + private Map<String, Object> introspectAccessToken(String token, Set<String> callerScopes) { |
136 | 144 | try {
|
137 |
| - |
138 | 145 | // check access tokens first (includes ID tokens)
|
139 |
| - accessToken = tokenServices.readAccessToken(tokenValue); |
140 |
| - |
141 |
| - tokenClient = accessToken.getClient(); |
| 146 | + OAuth2AccessTokenEntity accessToken = tokenServices.readAccessToken(token); |
| 147 | + ClientDetailsEntity tokenClient = accessToken.getClient(); |
142 | 148 |
|
143 | 149 | // get the user information of the user that authorized this token in the first place
|
144 | 150 | String userName = accessToken.getAuthenticationHolder().getAuthentication().getName();
|
145 |
| - user = userInfoService.get(userName, tokenClient.getClientId(), |
146 |
| - authScopes, accessToken.getAuthenticationHolder().getUserAuth()); |
147 |
| - |
| 151 | + UserInfo user = userInfoService.get(userName, tokenClient.getClientId(), |
| 152 | + callerScopes, accessToken.getAuthenticationHolder().getUserAuth()); |
| 153 | + return introspectionResultAssembler.assembleFrom(accessToken, user, callerScopes); |
148 | 154 | } catch (InvalidTokenException e) {
|
149 |
| - log.info("Invalid access token. Checking refresh token."); |
150 |
| - try { |
151 |
| - |
152 |
| - // check refresh tokens next |
153 |
| - refreshToken = tokenServices.getRefreshToken(tokenValue); |
154 |
| - |
155 |
| - tokenClient = refreshToken.getClient(); |
156 |
| - |
157 |
| - // get the user information of the user that authorized this token in the first place |
158 |
| - String userName = refreshToken.getAuthenticationHolder().getAuthentication().getName(); |
159 |
| - user = userInfoService.get(userName, tokenClient.getClientId(), authScopes, |
160 |
| - refreshToken.getAuthenticationHolder().getUserAuth()); |
161 |
| - |
162 |
| - } catch (InvalidTokenException e2) { |
163 |
| - log.error("Invalid refresh token"); |
164 |
| - Map<String,Boolean> entity = ImmutableMap.of(IntrospectionResultAssembler.ACTIVE, Boolean.FALSE); |
165 |
| - model.addAttribute(JsonEntityView.ENTITY, entity); |
166 |
| - return JsonEntityView.VIEWNAME; |
167 |
| - } |
| 155 | + return null; |
168 | 156 | }
|
| 157 | + } |
169 | 158 |
|
170 |
| - // if it's a valid token, we'll print out information on it |
| 159 | + private Map<String, Object> introspectRefreshToken(String token, Set<String> callerScopes) { |
| 160 | + try { |
| 161 | + OAuth2RefreshTokenEntity refreshToken = tokenServices.getRefreshToken(token); |
| 162 | + ClientDetailsEntity tokenClient = refreshToken.getClient(); |
171 | 163 |
|
172 |
| - if (accessToken != null) { |
173 |
| - Map<String, Object> entity = introspectionResultAssembler.assembleFrom(accessToken, user, authScopes); |
174 |
| - model.addAttribute(JsonEntityView.ENTITY, entity); |
175 |
| - } else if (refreshToken != null) { |
176 |
| - Map<String, Object> entity = introspectionResultAssembler.assembleFrom(refreshToken, user, authScopes); |
177 |
| - model.addAttribute(JsonEntityView.ENTITY, entity); |
178 |
| - } else { |
179 |
| - // no tokens were found (we shouldn't get here) |
180 |
| - log.error("Verify failed; Invalid access/refresh token"); |
181 |
| - Map<String,Boolean> entity = ImmutableMap.of(IntrospectionResultAssembler.ACTIVE, Boolean.FALSE); |
182 |
| - model.addAttribute(JsonEntityView.ENTITY, entity); |
183 |
| - return JsonEntityView.VIEWNAME; |
| 164 | + // get the user information of the user that authorized this token in the first place |
| 165 | + String userName = refreshToken.getAuthenticationHolder().getAuthentication().getName(); |
| 166 | + UserInfo user = userInfoService.get(userName, tokenClient.getClientId(), callerScopes, |
| 167 | + refreshToken.getAuthenticationHolder().getUserAuth()); |
| 168 | + return introspectionResultAssembler.assembleFrom(refreshToken, user, callerScopes); |
| 169 | + } catch (InvalidTokenException e2) { |
| 170 | + return null; |
184 | 171 | }
|
| 172 | + } |
185 | 173 |
|
186 |
| - return JsonEntityView.VIEWNAME; |
| 174 | + private String codeErrorResponse(Model model, HttpStatus code) { |
| 175 | + model.addAttribute(HttpCodeView.CODE, code); |
| 176 | + return HttpCodeView.VIEWNAME; |
| 177 | + } |
187 | 178 |
|
| 179 | + private String jsonResponse(Model model, Map<String, Object> entity) { |
| 180 | + model.addAttribute(JsonEntityView.ENTITY, entity); |
| 181 | + return JsonEntityView.VIEWNAME; |
188 | 182 | }
|
189 | 183 |
|
190 | 184 | }
|
0 commit comments