1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.sso.oic;
17
18 import java.io.IOException;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Map;
24
25 import org.apache.logging.log4j.LogManager;
26 import org.apache.logging.log4j.Logger;
27 import org.codelibs.core.lang.StringUtil;
28 import org.codelibs.core.misc.DynamicProperties;
29 import org.codelibs.core.net.UuidUtil;
30 import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
31 import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver;
32 import org.codelibs.fess.app.web.base.login.OpenIdConnectCredential;
33 import org.codelibs.fess.crawler.Constants;
34 import org.codelibs.fess.mylasta.action.FessUserBean;
35 import org.codelibs.fess.sso.SsoAuthenticator;
36 import org.codelibs.fess.sso.SsoResponseType;
37 import org.codelibs.fess.util.ComponentUtil;
38 import org.dbflute.optional.OptionalEntity;
39 import org.lastaflute.web.login.credential.LoginCredential;
40 import org.lastaflute.web.response.ActionResponse;
41 import org.lastaflute.web.response.HtmlResponse;
42 import org.lastaflute.web.util.LaRequestUtil;
43
44 import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
45 import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest;
46 import com.google.api.client.auth.oauth2.TokenResponse;
47 import com.google.api.client.http.GenericUrl;
48 import com.google.api.client.http.HttpTransport;
49 import com.google.api.client.http.javanet.NetHttpTransport;
50 import com.google.api.client.json.JsonFactory;
51 import com.google.api.client.json.JsonParser;
52 import com.google.api.client.json.JsonToken;
53 import com.google.api.client.json.gson.GsonFactory;
54 import com.google.common.io.BaseEncoding;
55 import com.google.common.io.BaseEncoding.DecodingException;
56
57 import jakarta.annotation.PostConstruct;
58 import jakarta.servlet.http.HttpServletRequest;
59 import jakarta.servlet.http.HttpSession;
60
61
62
63
64 public class OpenIdConnectAuthenticator implements SsoAuthenticator {
65
66
67
68
69 public OpenIdConnectAuthenticator() {
70
71 }
72
73 private static final Logger logger = LogManager.getLogger(OpenIdConnectAuthenticator.class);
74
75 private static final BaseEncoding BASE64_DECODER = BaseEncoding.base64().withSeparator("\n", 64);
76
77 private static final BaseEncoding BASE64URL_DECODER = BaseEncoding.base64Url().withSeparator("\n", 64);
78
79
80 protected static final String OIC_AUTH_SERVER_URL = "oic.auth.server.url";
81
82
83 protected static final String OIC_CLIENT_ID = "oic.client.id";
84
85
86 protected static final String OIC_SCOPE = "oic.scope";
87
88
89 protected static final String OIC_REDIRECT_URL = "oic.redirect.url";
90
91
92 protected static final String OIC_TOKEN_SERVER_URL = "oic.token.server.url";
93
94
95 protected static final String OIC_CLIENT_SECRET = "oic.client.secret";
96
97
98 protected static final String OIC_STATE = "OIC_STATE";
99
100
101 protected static final String OIC_BASE_URL = "oic.base.url";
102
103
104 protected final HttpTransport httpTransport = new NetHttpTransport();
105
106
107 protected final JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
108
109
110
111
112 @PostConstruct
113 public void init() {
114 if (logger.isDebugEnabled()) {
115 logger.debug("Initializing {}", this.getClass().getSimpleName());
116 }
117 ComponentUtil.getSsoManager().register(this);
118 }
119
120 @Override
121 public LoginCredential getLoginCredential() {
122 return LaRequestUtil.getOptionalRequest().map(request -> {
123 if (logger.isDebugEnabled()) {
124 logger.debug("Logging in with OpenID Connect Authenticator");
125 }
126 final HttpSession session = request.getSession(false);
127 if (session != null) {
128 final String sesState = (String) session.getAttribute(OIC_STATE);
129 if (StringUtil.isNotBlank(sesState)) {
130 session.removeAttribute(OIC_STATE);
131 final String code = request.getParameter("code");
132 final String reqState = request.getParameter("state");
133 if (logger.isDebugEnabled()) {
134 logger.debug("code: {}, state(request): {}, state(session): {}", code, reqState, sesState);
135 }
136 if (sesState.equals(reqState) && StringUtil.isNotBlank(code)) {
137 return processCallback(request, code);
138 }
139 }
140 }
141
142 return new ActionResponseCredential(() -> HtmlResponse.fromRedirectPathAsIs(getAuthUrl(request)));
143 }).orElse(null);
144 }
145
146
147
148
149
150
151
152 protected String getAuthUrl(final HttpServletRequest request) {
153 final String state = UuidUtil.create();
154 request.getSession().setAttribute(OIC_STATE, state);
155 return new AuthorizationCodeRequestUrl(getOicAuthServerUrl(), getOicClientId())
156 .setScopes(Arrays.asList(getOicScope()))
157 .setResponseTypes(Arrays.asList("code"))
158 .setRedirectUri(getOicRedirectUrl())
159 .setState(state)
160 .build();
161 }
162
163
164
165
166
167
168
169 protected byte[] decodeBase64(String base64String) {
170 if (base64String == null) {
171 return null;
172 }
173 try {
174 return BASE64_DECODER.decode(base64String);
175 } catch (IllegalArgumentException e) {
176 if (e.getCause() instanceof DecodingException) {
177 return BASE64URL_DECODER.decode(base64String.trim());
178 }
179 throw e;
180 }
181 }
182
183
184
185
186
187
188
189
190 protected LoginCredential processCallback(final HttpServletRequest request, final String code) {
191 try {
192 final TokenResponse tr = getTokenUrl(code);
193
194 final String[] jwt = ((String) tr.get("id_token")).split("\\.");
195 final String jwtHeader = new String(decodeBase64(jwt[0]), Constants.UTF_8_CHARSET);
196 final String jwtClaim = new String(decodeBase64(jwt[1]), Constants.UTF_8_CHARSET);
197 final String jwtSignature = new String(decodeBase64(jwt[2]), Constants.UTF_8_CHARSET);
198
199 if (logger.isDebugEnabled()) {
200 logger.debug("jwtHeader={}", jwtHeader);
201 logger.debug("jwtClaim={}", jwtClaim);
202 logger.debug("jwtSignature={}", jwtSignature);
203 }
204
205
206
207
208
209
210 final Map<String, Object> attributes = new HashMap<>();
211 attributes.put("accesstoken", tr.getAccessToken());
212 attributes.put("refreshtoken", tr.getRefreshToken() == null ? "null" : tr.getRefreshToken());
213 attributes.put("tokentype", tr.getTokenType());
214 attributes.put("expire", tr.getExpiresInSeconds());
215 attributes.put("jwtheader", jwtHeader);
216 attributes.put("jwtclaim", jwtClaim);
217 attributes.put("jwtsignature", jwtSignature);
218
219 if (logger.isDebugEnabled()) {
220 logger.debug("attributes={}", attributes);
221 }
222 parseJwtClaim(jwtClaim, attributes);
223
224 return new OpenIdConnectCredential(attributes);
225 } catch (final IOException e) {
226 if (logger.isDebugEnabled()) {
227 logger.debug("Failed to process callback request.", e);
228 }
229 }
230 return null;
231 }
232
233
234
235
236
237
238
239
240 protected void parseJwtClaim(final String jwtClaim, final Map<String, Object> attributes) throws IOException {
241 try (final JsonParser jsonParser = jsonFactory.createJsonParser(jwtClaim)) {
242 while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
243 final String name = jsonParser.getCurrentName();
244 if (name != null) {
245 jsonParser.nextToken();
246
247 if (jsonParser.getCurrentToken() == JsonToken.START_ARRAY) {
248
249 attributes.put(name, parseArray(jsonParser));
250 } else if (jsonParser.getCurrentToken() == JsonToken.START_OBJECT) {
251
252 attributes.put(name, parseObject(jsonParser));
253 } else {
254
255 attributes.put(name, parsePrimitive(jsonParser));
256 }
257 }
258 }
259 }
260 }
261
262
263
264
265
266
267
268
269 protected Object parsePrimitive(final JsonParser jsonParser) throws IOException {
270 final JsonToken token = jsonParser.getCurrentToken();
271 return switch (token) {
272 case VALUE_STRING -> jsonParser.getText();
273 case VALUE_NUMBER_INT -> jsonParser.getLongValue();
274 case VALUE_NUMBER_FLOAT -> jsonParser.getDoubleValue();
275 case VALUE_TRUE -> true;
276 case VALUE_FALSE -> false;
277 case VALUE_NULL -> null;
278 default -> null;
279 };
280 }
281
282
283
284
285
286
287
288
289 protected Object parseArray(final JsonParser jsonParser) throws IOException {
290 final List<Object> list = new ArrayList<>();
291 while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
292 if (jsonParser.getCurrentToken() == JsonToken.START_OBJECT) {
293 list.add(parseObject(jsonParser));
294 } else if (jsonParser.getCurrentToken() == JsonToken.START_ARRAY) {
295 list.add(parseArray(jsonParser));
296 } else {
297 list.add(parsePrimitive(jsonParser));
298 }
299 }
300
301 return list;
302 }
303
304
305
306
307
308
309
310
311 protected Map<String, Object> parseObject(final JsonParser jsonParser) throws IOException {
312 final Map<String, Object> nestedMap = new HashMap<>();
313 while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
314 final String fieldName = jsonParser.getCurrentName();
315 if (fieldName != null) {
316 jsonParser.nextToken();
317
318 if (jsonParser.getCurrentToken() == JsonToken.START_ARRAY) {
319 nestedMap.put(fieldName, parseArray(jsonParser));
320 } else if (jsonParser.getCurrentToken() == JsonToken.START_OBJECT) {
321 nestedMap.put(fieldName, parseObject(jsonParser));
322 } else {
323 nestedMap.put(fieldName, parsePrimitive(jsonParser));
324 }
325 }
326 }
327 return nestedMap;
328 }
329
330
331
332
333
334
335
336
337 protected TokenResponse getTokenUrl(final String code) throws IOException {
338 return new AuthorizationCodeTokenRequest(httpTransport, jsonFactory, new GenericUrl(getOicTokenServerUrl()), code)
339 .setGrantType("authorization_code")
340 .setRedirectUri(getOicRedirectUrl())
341 .set("client_id", getOicClientId())
342 .set("client_secret", getOicClientSecret())
343 .execute();
344 }
345
346
347
348
349
350
351 protected String getOicClientSecret() {
352 return ComponentUtil.getSystemProperties().getProperty(OIC_CLIENT_SECRET, StringUtil.EMPTY);
353 }
354
355
356
357
358
359
360 protected String getOicTokenServerUrl() {
361 return ComponentUtil.getSystemProperties().getProperty(OIC_TOKEN_SERVER_URL, "https://accounts.google.com/o/oauth2/token");
362 }
363
364
365
366
367
368
369 protected String getOicRedirectUrl() {
370 return ComponentUtil.getSystemProperties().getProperty(OIC_REDIRECT_URL, buildDefaultRedirectUrl());
371 }
372
373
374
375
376
377
378
379
380 protected String buildDefaultRedirectUrl() {
381 final DynamicProperties systemProperties = ComponentUtil.getSystemProperties();
382 String baseUrl = systemProperties.getProperty(OIC_BASE_URL);
383 if (StringUtil.isBlank(baseUrl)) {
384 baseUrl = "http://localhost:8080";
385 }
386 if (baseUrl.endsWith("/")) {
387 baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
388 }
389 return baseUrl + "/sso/";
390 }
391
392
393
394
395
396
397 protected String getOicScope() {
398 return ComponentUtil.getSystemProperties().getProperty(OIC_SCOPE, StringUtil.EMPTY);
399 }
400
401
402
403
404
405
406 protected String getOicClientId() {
407 return ComponentUtil.getSystemProperties().getProperty(OIC_CLIENT_ID, StringUtil.EMPTY);
408 }
409
410
411
412
413
414
415 protected String getOicAuthServerUrl() {
416 return ComponentUtil.getSystemProperties().getProperty(OIC_AUTH_SERVER_URL, "https://accounts.google.com/o/oauth2/auth");
417 }
418
419 @Override
420 public void resolveCredential(final LoginCredentialResolver resolver) {
421 resolver.resolve(OpenIdConnectCredential.class, credential -> OptionalEntity.of(credential.getUser()));
422 }
423
424 @Override
425 public ActionResponse getResponse(final SsoResponseType responseType) {
426 return null;
427 }
428
429 @Override
430 public String logout(final FessUserBean user) {
431 return null;
432 }
433 }