192da63714c977f399bcc8d19d8047b94a9841bd
[ccsdk/features.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP : ccsdk features
4  * ================================================================================
5  * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property.
6  * All rights reserved.
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *     http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  *
21  */
22 package org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers;
23
24 import com.auth0.jwt.JWT;
25 import com.auth0.jwt.exceptions.JWTDecodeException;
26 import com.auth0.jwt.interfaces.DecodedJWT;
27 import com.fasterxml.jackson.core.JsonProcessingException;
28 import com.fasterxml.jackson.databind.DeserializationFeature;
29 import com.fasterxml.jackson.databind.JsonMappingException;
30 import com.fasterxml.jackson.databind.ObjectMapper;
31 import java.io.IOException;
32 import java.net.URLEncoder;
33 import java.nio.charset.StandardCharsets;
34 import java.util.Base64;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Map.Entry;
39 import java.util.Optional;
40 import java.util.stream.Collectors;
41 import javax.servlet.ServletOutputStream;
42 import javax.servlet.http.HttpServletRequest;
43 import javax.servlet.http.HttpServletResponse;
44
45 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.*;
46 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.AuthHttpServlet;
47 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.client.MappedBaseHttpResponse;
48 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.client.MappingBaseHttpClient;
49 import org.apache.shiro.authc.BearerToken;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 public abstract class AuthService {
54
55
56     private static final Logger LOG = LoggerFactory.getLogger(AuthService.class);
57     private final MappingBaseHttpClient httpClient;
58     protected final ObjectMapper mapper;
59     protected final OAuthProviderConfig config;
60     protected final TokenCreator tokenCreator;
61     private final String redirectUri;
62     private final String tokenEndpoint;
63     private final String authEndpoint;
64
65     protected abstract String getTokenVerifierUri();
66
67     protected abstract Map<String, String> getAdditionalTokenVerifierParams();
68
69     protected abstract ResponseType getResponseType();
70
71     protected abstract boolean doSeperateRolesRequest();
72
73     protected abstract UserTokenPayload mapAccessToken(String spayload)
74             throws JsonMappingException, JsonProcessingException;
75
76     protected abstract String getLoginUrl(String callbackUrl);
77
78     protected abstract UserTokenPayload requestUserRoles(String access_token, long issued_at, long expires_at);
79
80     protected abstract boolean verifyState(String state);
81
82     public AuthService(OAuthProviderConfig config, String redirectUri, TokenCreator tokenCreator) throws UnableToConfigureOAuthService {
83         this.config = config;
84         this.tokenCreator = tokenCreator;
85         this.redirectUri = redirectUri;
86         this.mapper = new ObjectMapper();
87         this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
88         this.httpClient = new MappingBaseHttpClient(this.config.getUrlOrInternal(), this.config.trustAll());
89         if (this.config.hasToBeConfigured()){
90             Optional<MappedBaseHttpResponse<OpenIdConfigResponseData>> oresponse = this.httpClient.sendMappedRequest(
91                     this.config.getOpenIdConfigUrl(), "GET", null, null, OpenIdConfigResponseData.class);
92             if(oresponse.isEmpty()){
93                 throw new UnableToConfigureOAuthService(this.config.getOpenIdConfigUrl());
94             }
95             MappedBaseHttpResponse<OpenIdConfigResponseData> response = oresponse.get();
96             if(!response.isSuccess()){
97                 throw new UnableToConfigureOAuthService(this.config.getOpenIdConfigUrl(), response.code);
98             }
99             this.tokenEndpoint = response.body.getToken_endpoint();
100             this.authEndpoint = response.body.getAuthorization_endpoint();
101         }
102         else{
103             this.tokenEndpoint = null;
104             this.authEndpoint = null;
105         }
106     }
107
108     public PublicOAuthProviderConfig getConfig() {
109         return new PublicOAuthProviderConfig(this);
110     }
111
112     protected MappingBaseHttpClient getHttpClient() {
113         return this.httpClient;
114     }
115
116     public void handleRedirect(HttpServletRequest req, HttpServletResponse resp, String host) throws IOException {
117         switch (this.getResponseType()) {
118             case CODE:
119                 this.handleRedirectCode(req, resp, host);
120                 break;
121             case TOKEN:
122                 sendErrorResponse(resp, "not yet implemented");
123                 break;
124             case SESSION_STATE:
125                 break;
126         }
127     }
128
129     public void sendLoginRedirectResponse(HttpServletResponse resp, String callbackUrl) {
130         resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
131         String url = this.authEndpoint!=null?String.format(
132                 "%s?client_id=%s&response_type=code&scope=%s&redirect_uri=%s",
133                 this.authEndpoint, urlEncode(this.config.getClientId()), this.config.getScope(),
134                 urlEncode(callbackUrl)):this.getLoginUrl(callbackUrl);
135         resp.setHeader("Location", url);
136     }
137
138     private static void sendErrorResponse(HttpServletResponse resp, String message) throws IOException {
139         resp.sendError(HttpServletResponse.SC_NOT_FOUND, message);
140     }
141
142     private void handleRedirectCode(HttpServletRequest req, HttpServletResponse resp, String host) throws IOException {
143         final String code = req.getParameter("code");
144         final String state = req.getParameter("state");
145         OAuthResponseData response = null;
146         if(this.verifyState(state)) {
147             response = this.getTokenForUser(code, host);
148         }
149         if (response != null) {
150             if (this.doSeperateRolesRequest()) {
151                 //long expiresAt = this.tokenCreator.getDefaultExp(Math.round(response.getExpires_in()));
152                 long expiresAt = this.tokenCreator.getDefaultExp();
153                 long issuedAt = this.tokenCreator.getDefaultIat();
154                 UserTokenPayload data = this.requestUserRoles(response.getAccess_token(), issuedAt, expiresAt);
155                 if (data != null) {
156                     this.handleUserInfoToken(data, resp, host);
157                 } else {
158                     sendErrorResponse(resp, "unable to verify user");
159                 }
160             } else {
161                 this.handleUserInfoToken(response.getAccess_token(), resp, host);
162             }
163         } else {
164             sendErrorResponse(resp, "unable to verify code");
165         }
166     }
167
168     private void handleUserInfoToken(UserTokenPayload data, HttpServletResponse resp, String localHostUrl)
169             throws IOException {
170         BearerToken onapToken = this.tokenCreator.createNewJWT(data);
171         sendTokenResponse(resp, onapToken, localHostUrl);
172     }
173
174     private void handleUserInfoToken(String accessToken, HttpServletResponse resp, String localHostUrl)
175             throws IOException {
176         try {
177             DecodedJWT jwt = JWT.decode(accessToken);
178             String spayload = base64Decode(jwt.getPayload());
179             LOG.debug("payload in jwt='{}'", spayload);
180             UserTokenPayload data = this.mapAccessToken(spayload);
181             this.handleUserInfoToken(data, resp, localHostUrl);
182         } catch (JWTDecodeException | JsonProcessingException e) {
183             LOG.warn("unable to decode jwt token {}: ", accessToken, e);
184             sendErrorResponse(resp, e.getMessage());
185         }
186     }
187
188
189     protected List<String> mapRoles(List<String> roles) {
190         final Map<String, String> map = this.config.getRoleMapping();
191         return roles.stream().map(r -> map.getOrDefault(r, r)).collect(Collectors.toList());
192     }
193
194     private void sendTokenResponse(HttpServletResponse resp, BearerToken data, String localHostUrl) throws IOException {
195         if (this.redirectUri == null) {
196             byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
197             resp.setStatus(200);
198             resp.setContentLength(output.length);
199             resp.setContentType("application/json");
200             ServletOutputStream os = null;
201             os = resp.getOutputStream();
202             os.write(output);
203         } else {
204             resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
205             resp.setHeader("Location", assembleUrl(localHostUrl, this.redirectUri, data.getToken()));
206         }
207     }
208
209
210
211     private static String base64Decode(String data) {
212         return new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8);
213     }
214
215     private OAuthResponseData getTokenForUser(String code, String localHostUrl) {
216
217         Map<String, String> headers = new HashMap<>();
218         headers.put("Content-Type", "application/x-www-form-urlencoded");
219         Map<String, String> params = this.getAdditionalTokenVerifierParams();
220         params.put("code", code);
221         params.put("client_id", this.config.getClientId());
222         params.put("client_secret", this.config.getSecret());
223         params.put("redirect_uri", assembleRedirectUrl(localHostUrl, AuthHttpServlet.REDIRECTURI, this.config.getId()));
224         StringBuilder body = new StringBuilder();
225         for (Entry<String, String> p : params.entrySet()) {
226             body.append(String.format("%s=%s&", p.getKey(), urlEncode(p.getValue())));
227         }
228
229         String url = this.tokenEndpoint!=null?this.tokenEndpoint:this.getTokenVerifierUri();
230         Optional<MappedBaseHttpResponse<OAuthResponseData>> response =
231                 this.httpClient.sendMappedRequest(url, "POST",
232                         body.substring(0, body.length() - 1), headers, OAuthResponseData.class);
233         if (response.isPresent() && response.get().isSuccess()) {
234             return response.get().body;
235         }
236         LOG.warn("problem get token for code {}", code);
237
238         return null;
239     }
240
241     /**
242      * Assemble callback url for service provider {host}{baseUri}/{serviceId} e.g.
243      * http://10.20.0.11:8181/oauth/redirect/keycloak
244      *
245      * @param host
246      * @param baseUri
247      * @param serviceId
248      * @return
249      */
250     public static String assembleRedirectUrl(String host, String baseUri, String serviceId) {
251         return String.format("%s%s/%s", host, baseUri, serviceId);
252     }
253
254     private static String assembleUrl(String host, String uri, String token) {
255         return String.format("%s%s%s", host, uri, token);
256     }
257
258     public static String urlEncode(String s) {
259         return URLEncoder.encode(s, StandardCharsets.UTF_8);
260     }
261
262     public enum ResponseType {
263         CODE, TOKEN, SESSION_STATE
264     }
265
266
267     public static class PublicOAuthProviderConfig {
268
269         private String id;
270         private String title;
271         private String loginUrl;
272
273         public String getId() {
274             return id;
275         }
276
277         public void setId(String id) {
278             this.id = id;
279         }
280
281         public String getTitle() {
282             return title;
283         }
284
285         public void setTitle(String title) {
286             this.title = title;
287         }
288
289         public String getLoginUrl() {
290             return loginUrl;
291         }
292
293         public void setLoginUrl(String loginUrl) {
294             this.loginUrl = loginUrl;
295         }
296
297         public PublicOAuthProviderConfig(AuthService authService) {
298             this.id = authService.config.getId();
299             this.title = authService.config.getTitle();
300             this.loginUrl = String.format(AuthHttpServlet.LOGIN_REDIRECT_FORMAT, authService.config.getId());
301         }
302
303     }
304
305
306
307 }