1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.forgerock.http.oauth2.resolver;
18
19 import static java.lang.String.format;
20 import static java.util.concurrent.TimeUnit.MILLISECONDS;
21 import static java.util.concurrent.TimeUnit.SECONDS;
22 import static org.forgerock.json.JsonValueFunctions.setOf;
23 import static org.forgerock.util.Utils.closeSilently;
24
25 import java.io.IOException;
26 import java.net.URI;
27 import java.net.URISyntaxException;
28 import java.util.Set;
29
30 import org.forgerock.http.oauth2.AccessTokenException;
31 import org.forgerock.http.oauth2.AccessTokenInfo;
32 import org.forgerock.http.oauth2.AccessTokenResolver;
33 import org.forgerock.http.Handler;
34 import org.forgerock.http.protocol.Entity;
35 import org.forgerock.http.protocol.Form;
36 import org.forgerock.http.protocol.Request;
37 import org.forgerock.http.protocol.Response;
38 import org.forgerock.http.protocol.Responses;
39 import org.forgerock.http.protocol.Status;
40 import org.forgerock.json.JsonValue;
41 import org.forgerock.json.JsonValueException;
42 import org.forgerock.services.context.Context;
43 import org.forgerock.util.Function;
44 import org.forgerock.util.annotations.VisibleForTesting;
45 import org.forgerock.util.promise.Promise;
46 import org.forgerock.util.promise.Promises;
47 import org.forgerock.util.time.TimeService;
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78 public class OpenAmAccessTokenResolver implements AccessTokenResolver {
79
80 private final Handler client;
81 private final String tokenInfoEndpoint;
82 private final Function<JsonValue, AccessTokenInfo, AccessTokenException> accessToken;
83
84
85
86
87
88
89
90
91
92
93
94
95 public OpenAmAccessTokenResolver(final Handler client,
96 final TimeService time,
97 final String tokenInfoEndpoint) {
98 this(client, new TokenInfoParser(time), tokenInfoEndpoint);
99 }
100
101 private OpenAmAccessTokenResolver(final Handler client,
102 final Function<JsonValue, AccessTokenInfo, AccessTokenException> accessToken,
103 final String tokenInfoEndpoint) {
104 this.client = client;
105 this.accessToken = accessToken;
106 this.tokenInfoEndpoint = tokenInfoEndpoint;
107 }
108
109 @Override
110 public Promise<AccessTokenInfo, AccessTokenException> resolve(Context context, final String token) {
111 try {
112 Request request = new Request();
113 request.setMethod("GET");
114 request.setUri(new URI(tokenInfoEndpoint));
115
116
117 Form form = new Form();
118 form.add("access_token", token);
119 form.toRequestQuery(request);
120
121
122 return client.handle(context, request)
123 .then(onResult(), Responses.<AccessTokenInfo, AccessTokenException>noopExceptionFunction());
124 } catch (URISyntaxException e) {
125 return Promises.newExceptionPromise(new AccessTokenException(
126 format("The token_info endpoint %s could not be accessed because it is a malformed URI",
127 tokenInfoEndpoint),
128 e));
129 }
130 }
131
132 private Function<Response, AccessTokenInfo, AccessTokenException> onResult() {
133 return new Function<Response, AccessTokenInfo, AccessTokenException>() {
134 @Override
135 public AccessTokenInfo apply(Response response) throws AccessTokenException {
136 if (isResponseEmpty(response)) {
137 throw new AccessTokenException("Authorization Server did not return any AccessToken");
138 }
139 JsonValue content = asJson(response.getEntity());
140 if (isOk(response)) {
141 return content.as(accessToken);
142 }
143
144 if (content.isDefined("error")) {
145 String error = content.get("error").asString();
146 String description = content.get("error_description").asString();
147 throw new AccessTokenException(format("Authorization Server returned an error "
148 + "(error: %s, description: %s)",
149 error,
150 description));
151 }
152
153 throw new AccessTokenException("AccessToken returned by the AuthorizationServer has a problem");
154 }
155 };
156 }
157
158 private boolean isResponseEmpty(final Response response) {
159
160 return response == null || response.getEntity() == null;
161 }
162
163 private boolean isOk(final Response response) {
164 return Status.OK.equals(response.getStatus());
165 }
166
167
168
169
170
171
172
173 private JsonValue asJson(final Entity entity) throws AccessTokenException {
174 try {
175 return new JsonValue(entity.getJson());
176 } catch (IOException e) {
177
178 throw new AccessTokenException("Cannot read response content as JSON", e);
179 } finally {
180 closeSilently(entity);
181 }
182 }
183
184 @VisibleForTesting
185 static class TokenInfoParser implements Function<JsonValue, AccessTokenInfo, AccessTokenException> {
186
187 private final TimeService time;
188
189
190
191
192
193
194
195 TokenInfoParser(final TimeService time) {
196 this.time = time;
197 }
198
199
200
201
202
203
204
205
206
207
208
209 @Override
210 public AccessTokenInfo apply(final JsonValue raw) throws AccessTokenException {
211 try {
212 final long expiresIn = raw.get("expires_in").required().asLong();
213 final Set<String> scopes = raw.get("scope").required().as(setOf(String.class));
214 final String token = raw.get("access_token").required().asString();
215
216 return new AccessTokenInfo(raw, token, scopes, getExpirationTime(expiresIn));
217 } catch (JsonValueException | NullPointerException e) {
218 throw new AccessTokenException("Cannot build AccessToken from the given JSON: invalid format", e);
219 }
220 }
221
222 private long getExpirationTime(final long delayInSeconds) {
223 return time.now() + MILLISECONDS.convert(delayInSeconds, SECONDS);
224 }
225 }
226 }