View Javadoc
1   /*
2    * Copyright 2012-2019 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.codelibs.core.lang.StringUtil;
28  import org.codelibs.core.net.UuidUtil;
29  import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
30  import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver;
31  import org.codelibs.fess.app.web.base.login.OpenIdConnectCredential;
32  import org.codelibs.fess.crawler.Constants;
33  import org.codelibs.fess.sso.SsoAuthenticator;
34  import org.codelibs.fess.util.ComponentUtil;
35  import org.dbflute.optional.OptionalEntity;
36  import org.lastaflute.web.login.credential.LoginCredential;
37  import org.lastaflute.web.response.HtmlResponse;
38  import org.lastaflute.web.util.LaRequestUtil;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
43  import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest;
44  import com.google.api.client.auth.oauth2.TokenResponse;
45  import com.google.api.client.http.GenericUrl;
46  import com.google.api.client.http.HttpTransport;
47  import com.google.api.client.http.javanet.NetHttpTransport;
48  import com.google.api.client.json.JsonFactory;
49  import com.google.api.client.json.JsonParser;
50  import com.google.api.client.json.JsonToken;
51  import com.google.api.client.json.jackson2.JacksonFactory;
52  import com.google.api.client.util.Base64;
53  
54  public class OpenIdConnectAuthenticator implements SsoAuthenticator {
55  
56      private static final Logger logger = LoggerFactory.getLogger(OpenIdConnectAuthenticator.class);
57  
58      protected static final String OIC_AUTH_SERVER_URL = "oic.auth.server.url";
59  
60      protected static final String OIC_CLIENT_ID = "oic.client.id";
61  
62      protected static final String OIC_SCOPE = "oic.scope";
63  
64      protected static final String OIC_REDIRECT_URL = "oic.redirect.url";
65  
66      protected static final String OIC_TOKEN_SERVER_URL = "oic.token.server.url";
67  
68      protected static final String OIC_CLIENT_SECRET = "oic.client.secret";
69  
70      protected static final String OIC_STATE = "OIC_STATE";
71  
72      protected final HttpTransport httpTransport = new NetHttpTransport();
73  
74      protected final JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
75  
76      @PostConstruct
77      public void init() {
78          ComponentUtil.getSsoManager().register(this);
79      }
80  
81      @Override
82      public LoginCredential getLoginCredential() {
83          return LaRequestUtil.getOptionalRequest().map(request -> {
84              final HttpSession session = request.getSession(false);
85              if (session != null) {
86                  final String sesState = (String) session.getAttribute(OIC_STATE);
87                  if (StringUtil.isNotBlank(sesState)) {
88                      session.removeAttribute(OIC_STATE);
89                      final String code = request.getParameter("code");
90                      final String reqState = request.getParameter("state");
91                      if (sesState.equals(reqState) && StringUtil.isNotBlank(code)) {
92                          return processCallback(request, code);
93                      }
94                      if (logger.isDebugEnabled()) {
95                          logger.debug("code:" + code + " state(request):" + reqState + " state(session):" + sesState);
96                      }
97                      return null;
98                  }
99              }
100 
101             return new ActionResponseCredential(() -> HtmlResponse.fromRedirectPathAsIs(getAuthUrl(request)));
102         }).orElse(null);
103     }
104 
105     protected String getAuthUrl(final HttpServletRequest request) {
106         final String state = UuidUtil.create();
107         request.getSession().setAttribute(OIC_STATE, state);
108         return new AuthorizationCodeRequestUrl(getOicAuthServerUrl(), getOicClientId())//
109                 .setScopes(Arrays.asList(getOicScope()))//
110                 .setResponseTypes(Arrays.asList("code"))//
111                 .setRedirectUri(getOicRedirectUrl())//
112                 .setState(state)//
113                 .build();
114     }
115 
116     protected LoginCredential processCallback(final HttpServletRequest request, final String code) {
117         try {
118             final TokenResponse tr = getTokenUrl(code);
119 
120             final String[] jwt = ((String) tr.get("id_token")).split("\\.");
121             final String jwtHeader = new String(Base64.decodeBase64(jwt[0]), Constants.UTF_8_CHARSET);
122             final String jwtClaim = new String(Base64.decodeBase64(jwt[1]), Constants.UTF_8_CHARSET);
123             final String jwtSigniture = new String(Base64.decodeBase64(jwt[2]), Constants.UTF_8_CHARSET);
124 
125             if (logger.isDebugEnabled()) {
126                 logger.debug("jwtHeader: " + jwtHeader);
127                 logger.debug("jwtClaim: " + jwtClaim);
128                 logger.debug("jwtSigniture: " + jwtSigniture);
129             }
130 
131             // TODO validate signiture
132 
133             final Map<String, Object> attributes = new HashMap<>();
134             attributes.put("accesstoken", tr.getAccessToken());
135             attributes.put("refreshtoken", tr.getRefreshToken() == null ? "null" : tr.getRefreshToken());
136             attributes.put("tokentype", tr.getTokenType());
137             attributes.put("expire", tr.getExpiresInSeconds());
138             attributes.put("jwtheader", jwtHeader);
139             attributes.put("jwtclaim", jwtClaim);
140             attributes.put("jwtsign", jwtSigniture);
141 
142             parseJwtClaim(jwtClaim, attributes);
143 
144             return new OpenIdConnectCredential(attributes);
145         } catch (final IOException e) {
146             if (logger.isDebugEnabled()) {
147                 logger.debug("Failed to process callbacked request.", e);
148             }
149         }
150         return null;
151     }
152 
153     protected void parseJwtClaim(final String jwtClaim, final Map<String, Object> attributes) throws IOException {
154         final JsonParser jsonParser = jsonFactory.createJsonParser(jwtClaim);
155         while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
156             final String name = jsonParser.getCurrentName();
157             if (name != null) {
158                 jsonParser.nextToken();
159 
160                 // TODO other parameters
161                 switch (name) {
162                 case "iss":
163                     attributes.put("iss", jsonParser.getText());
164                     break;
165                 case "sub":
166                     attributes.put("sub", jsonParser.getText());
167                     break;
168                 case "azp":
169                     attributes.put("azp", jsonParser.getText());
170                     break;
171                 case "email":
172                     attributes.put("email", jsonParser.getText());
173                     break;
174                 case "at_hash":
175                     attributes.put("at_hash", jsonParser.getText());
176                     break;
177                 case "email_verified":
178                     attributes.put("email_verified", jsonParser.getText());
179                     break;
180                 case "aud":
181                     attributes.put("aud", jsonParser.getText());
182                     break;
183                 case "iat":
184                     attributes.put("iat", jsonParser.getText());
185                     break;
186                 case "exp":
187                     attributes.put("exp", jsonParser.getText());
188                     break;
189                 }
190             }
191         }
192     }
193 
194     protected TokenResponse getTokenUrl(final String code) throws IOException {
195         return new AuthorizationCodeTokenRequest(httpTransport, jsonFactory, new GenericUrl(getOicTokenServerUrl()), code)//
196                 .setGrantType("authorization_code")//
197                 .setRedirectUri(getOicRedirectUrl())//
198                 .set("client_id", getOicClientId())//
199                 .set("client_secret", getOicClientSecret())//
200                 .execute();
201     }
202 
203     protected String getOicClientSecret() {
204         return ComponentUtil.getSystemProperties().getProperty(OIC_CLIENT_SECRET, StringUtil.EMPTY);
205     }
206 
207     protected String getOicTokenServerUrl() {
208         return ComponentUtil.getSystemProperties().getProperty(OIC_TOKEN_SERVER_URL, "https://accounts.google.com/o/oauth2/token");
209     }
210 
211     protected String getOicRedirectUrl() {
212         return ComponentUtil.getSystemProperties().getProperty(OIC_REDIRECT_URL, "http://localhost:8080/sso/");
213     }
214 
215     protected String getOicScope() {
216         return ComponentUtil.getSystemProperties().getProperty(OIC_SCOPE, StringUtil.EMPTY);
217     }
218 
219     protected String getOicClientId() {
220         return ComponentUtil.getSystemProperties().getProperty(OIC_CLIENT_ID, StringUtil.EMPTY);
221     }
222 
223     protected String getOicAuthServerUrl() {
224         return ComponentUtil.getSystemProperties().getProperty(OIC_AUTH_SERVER_URL, "https://accounts.google.com/o/oauth2/auth");
225     }
226 
227     @Override
228     public void resolveCredential(final LoginCredentialResolver resolver) {
229         resolver.resolve(OpenIdConnectCredential.class, credential -> {
230             return OptionalEntity.of(credential.getUser());
231         });
232     }
233 }