|
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