View Javadoc
1   /*
2    * Copyright 2012-2021 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.sso.oic;
17  
18  import java.io.IOException;
19  import java.util.Arrays;
20  import java.util.HashMap;
21  import java.util.Map;
22  
23  import javax.annotation.PostConstruct;
24  import javax.servlet.http.HttpServletRequest;
25  import javax.servlet.http.HttpSession;
26  
27  import org.apache.logging.log4j.LogManager;
28  import org.apache.logging.log4j.Logger;
29  import org.codelibs.core.lang.StringUtil;
30  import org.codelibs.core.net.UuidUtil;
31  import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
32  import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver;
33  import org.codelibs.fess.app.web.base.login.OpenIdConnectCredential;
34  import org.codelibs.fess.crawler.Constants;
35  import org.codelibs.fess.mylasta.action.FessUserBean;
36  import org.codelibs.fess.sso.SsoAuthenticator;
37  import org.codelibs.fess.sso.SsoResponseType;
38  import org.codelibs.fess.util.ComponentUtil;
39  import org.dbflute.optional.OptionalEntity;
40  import org.lastaflute.web.login.credential.LoginCredential;
41  import org.lastaflute.web.response.ActionResponse;
42  import org.lastaflute.web.response.HtmlResponse;
43  import org.lastaflute.web.util.LaRequestUtil;
44  
45  import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
46  import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest;
47  import com.google.api.client.auth.oauth2.TokenResponse;
48  import com.google.api.client.http.GenericUrl;
49  import com.google.api.client.http.HttpTransport;
50  import com.google.api.client.http.javanet.NetHttpTransport;
51  import com.google.api.client.json.JsonFactory;
52  import com.google.api.client.json.JsonParser;
53  import com.google.api.client.json.JsonToken;
54  import com.google.api.client.json.jackson2.JacksonFactory;
55  import com.google.api.client.util.Base64;
56  
57  public class OpenIdConnectAuthenticator implements SsoAuthenticator {
58  
59      private static final Logger logger = LogManager.getLogger(OpenIdConnectAuthenticator.class);
60  
61      protected static final String OIC_AUTH_SERVER_URL = "oic.auth.server.url";
62  
63      protected static final String OIC_CLIENT_ID = "oic.client.id";
64  
65      protected static final String OIC_SCOPE = "oic.scope";
66  
67      protected static final String OIC_REDIRECT_URL = "oic.redirect.url";
68  
69      protected static final String OIC_TOKEN_SERVER_URL = "oic.token.server.url";
70  
71      protected static final String OIC_CLIENT_SECRET = "oic.client.secret";
72  
73      protected static final String OIC_STATE = "OIC_STATE";
74  
75      protected final HttpTransport httpTransport = new NetHttpTransport();
76  
77      protected final JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
78  
79      @PostConstruct
80      public void init() {
81          if (logger.isDebugEnabled()) {
82              logger.debug("Initialize {}", this.getClass().getSimpleName());
83          }
84          ComponentUtil.getSsoManager().register(this);
85      }
86  
87      @Override
88      public LoginCredential getLoginCredential() {
89          return LaRequestUtil.getOptionalRequest().map(request -> {
90              if (logger.isDebugEnabled()) {
91                  logger.debug("Logging in with OpenID Connect Authenticator");
92              }
93              final HttpSession session = request.getSession(false);
94              if (session != null) {
95                  final String sesState = (String) session.getAttribute(OIC_STATE);
96                  if (StringUtil.isNotBlank(sesState)) {
97                      session.removeAttribute(OIC_STATE);
98                      final String code = request.getParameter("code");
99                      final String reqState = request.getParameter("state");
100                     if (logger.isDebugEnabled()) {
101                         logger.debug("code: {}, state(request): {}, state(session): {}", code, reqState, sesState);
102                     }
103                     if (sesState.equals(reqState) && StringUtil.isNotBlank(code)) {
104                         return processCallback(request, code);
105                     }
106                     return null;
107                 }
108             }
109 
110             return new ActionResponseCredential(() -> HtmlResponse.fromRedirectPathAsIs(getAuthUrl(request)));
111         }).orElse(null);
112     }
113 
114     protected String getAuthUrl(final HttpServletRequest request) {
115         final String state = UuidUtil.create();
116         request.getSession().setAttribute(OIC_STATE, state);
117         return new AuthorizationCodeRequestUrl(getOicAuthServerUrl(), getOicClientId())//
118                 .setScopes(Arrays.asList(getOicScope()))//
119                 .setResponseTypes(Arrays.asList("code"))//
120                 .setRedirectUri(getOicRedirectUrl())//
121                 .setState(state)//
122                 .build();
123     }
124 
125     protected LoginCredential processCallback(final HttpServletRequest request, final String code) {
126         try {
127             final TokenResponse tr = getTokenUrl(code);
128 
129             final String[] jwt = ((String) tr.get("id_token")).split("\\.");
130             final String jwtHeader = new String(Base64.decodeBase64(jwt[0]), Constants.UTF_8_CHARSET);
131             final String jwtClaim = new String(Base64.decodeBase64(jwt[1]), Constants.UTF_8_CHARSET);
132             final String jwtSigniture = new String(Base64.decodeBase64(jwt[2]), Constants.UTF_8_CHARSET);
133 
134             if (logger.isDebugEnabled()) {
135                 logger.debug("jwtHeader: {}", jwtHeader);
136                 logger.debug("jwtClaim: {}", jwtClaim);
137                 logger.debug("jwtSigniture: {}", jwtSigniture);
138             }
139 
140             // TODO validate signiture
141 
142             final Map<String, Object> attributes = new HashMap<>();
143             attributes.put("accesstoken", tr.getAccessToken());
144             attributes.put("refreshtoken", tr.getRefreshToken() == null ? "null" : tr.getRefreshToken());
145             attributes.put("tokentype", tr.getTokenType());
146             attributes.put("expire", tr.getExpiresInSeconds());
147             attributes.put("jwtheader", jwtHeader);
148             attributes.put("jwtclaim", jwtClaim);
149             attributes.put("jwtsign", jwtSigniture);
150 
151             if (logger.isDebugEnabled()) {
152                 logger.debug("attribute: {}", attributes);
153             }
154             parseJwtClaim(jwtClaim, attributes);
155 
156             return new OpenIdConnectCredential(attributes);
157         } catch (final IOException e) {
158             if (logger.isDebugEnabled()) {
159                 logger.debug("Failed to process callbacked request.", e);
160             }
161         }
162         return null;
163     }
164 
165     protected void parseJwtClaim(final String jwtClaim, final Map<String, Object> attributes) throws IOException {
166         try (final JsonParser jsonParser = jsonFactory.createJsonParser(jwtClaim)) {
167             while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
168                 final String name = jsonParser.getCurrentName();
169                 if (name != null) {
170                     jsonParser.nextToken();
171 
172                     // TODO other parameters
173                     switch (name) {
174                     case "iss":
175                         attributes.put("iss", jsonParser.getText());
176                         break;
177                     case "sub":
178                         attributes.put("sub", jsonParser.getText());
179                         break;
180                     case "azp":
181                         attributes.put("azp", jsonParser.getText());
182                         break;
183                     case "email":
184                         attributes.put("email", jsonParser.getText());
185                         break;
186                     case "at_hash":
187                         attributes.put("at_hash", jsonParser.getText());
188                         break;
189                     case "email_verified":
190                         attributes.put("email_verified", jsonParser.getText());
191                         break;
192                     case "aud":
193                         attributes.put("aud", jsonParser.getText());
194                         break;
195                     case "iat":
196                         attributes.put("iat", jsonParser.getText());
197                         break;
198                     case "exp":
199                         attributes.put("exp", jsonParser.getText());
200                         break;
201                     }
202                 }
203             }
204         }
205     }
206 
207     protected TokenResponse getTokenUrl(final String code) throws IOException {
208         return new AuthorizationCodeTokenRequest(httpTransport, jsonFactory, new GenericUrl(getOicTokenServerUrl()), code)//
209                 .setGrantType("authorization_code")//
210                 .setRedirectUri(getOicRedirectUrl())//
211                 .set("client_id", getOicClientId())//
212                 .set("client_secret", getOicClientSecret())//
213                 .execute();
214     }
215 
216     protected String getOicClientSecret() {
217         return ComponentUtil.getSystemProperties().getProperty(OIC_CLIENT_SECRET, StringUtil.EMPTY);
218     }
219 
220     protected String getOicTokenServerUrl() {
221         return ComponentUtil.getSystemProperties().getProperty(OIC_TOKEN_SERVER_URL, "https://accounts.google.com/o/oauth2/token");
222     }
223 
224     protected String getOicRedirectUrl() {
225         return ComponentUtil.getSystemProperties().getProperty(OIC_REDIRECT_URL, "http://localhost:8080/sso/");
226     }
227 
228     protected String getOicScope() {
229         return ComponentUtil.getSystemProperties().getProperty(OIC_SCOPE, StringUtil.EMPTY);
230     }
231 
232     protected String getOicClientId() {
233         return ComponentUtil.getSystemProperties().getProperty(OIC_CLIENT_ID, StringUtil.EMPTY);
234     }
235 
236     protected String getOicAuthServerUrl() {
237         return ComponentUtil.getSystemProperties().getProperty(OIC_AUTH_SERVER_URL, "https://accounts.google.com/o/oauth2/auth");
238     }
239 
240     @Override
241     public void resolveCredential(final LoginCredentialResolver resolver) {
242         resolver.resolve(OpenIdConnectCredential.class, credential -> OptionalEntity.of(credential.getUser()));
243     }
244 
245     @Override
246     public ActionResponse getResponse(final SsoResponseType responseType) {
247         return null;
248     }
249 
250     @Override
251     public String logout(final FessUserBean user) {
252         return null;
253     }
254 }