3cb79757cec33e71f48377a3433e8d17caf7af05
[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 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthProviderConfig;
45 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthResponseData;
46 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.UserTokenPayload;
47 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.AuthHttpServlet;
48 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.client.MappedBaseHttpResponse;
49 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.client.MappingBaseHttpClient;
50 import org.opendaylight.aaa.shiro.filters.backport.BearerToken;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 public abstract class AuthService {
55
56
57     private static final Logger LOG = LoggerFactory.getLogger(AuthService.class);
58     private final MappingBaseHttpClient httpClient;
59     protected final ObjectMapper mapper;
60     protected final OAuthProviderConfig config;
61     protected final TokenCreator tokenCreator;
62     private final String redirectUri;
63
64     protected abstract String getTokenVerifierUri();
65
66     protected abstract Map<String, String> getAdditionalTokenVerifierParams();
67
68     protected abstract ResponseType getResponseType();
69
70     protected abstract boolean doSeperateRolesRequest();
71
72     protected abstract UserTokenPayload mapAccessToken(String spayload)
73             throws JsonMappingException, JsonProcessingException;
74
75     protected abstract String getLoginUrl(String callbackUrl);
76
77     protected abstract UserTokenPayload requestUserRoles(String access_token, long expires_at);
78
79     protected abstract boolean verifyState(String state);
80
81     public AuthService(OAuthProviderConfig config, String redirectUri, TokenCreator tokenCreator) {
82         this.config = config;
83         this.tokenCreator = tokenCreator;
84         this.redirectUri = redirectUri;
85         this.mapper = new ObjectMapper();
86         this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
87         this.httpClient = new MappingBaseHttpClient(this.config.getUrl());
88     }
89
90     public PublicOAuthProviderConfig getConfig() {
91         return new PublicOAuthProviderConfig(this);
92     }
93
94     protected MappingBaseHttpClient getHttpClient() {
95         return this.httpClient;
96     }
97
98     public void handleRedirect(HttpServletRequest req, HttpServletResponse resp, String host) throws IOException {
99         switch (this.getResponseType()) {
100             case CODE:
101                 this.handleRedirectCode(req, resp, host);
102                 break;
103             case TOKEN:
104                 sendErrorResponse(resp, "not yet implemented");
105                 break;
106             case SESSION_STATE:
107                 break;
108         }
109     }
110
111     public void sendLoginRedirectResponse(HttpServletResponse resp, String callbackUrl) {
112         resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
113         resp.setHeader("Location", this.getLoginUrl(callbackUrl));
114     }
115
116     private static void sendErrorResponse(HttpServletResponse resp, String message) throws IOException {
117         resp.sendError(HttpServletResponse.SC_NOT_FOUND, message);
118     }
119
120     private void handleRedirectCode(HttpServletRequest req, HttpServletResponse resp, String host) throws IOException {
121         final String code = req.getParameter("code");
122         final String state = req.getParameter("state");
123         OAuthResponseData response = null;
124         if(this.verifyState(state)) {
125             response = this.getTokenForUser(code, host);
126         }
127         if (response != null) {
128             if (this.doSeperateRolesRequest()) {
129                 //long expiresAt = this.tokenCreator.getDefaultExp(Math.round(response.getExpires_in()));
130                 long expiresAt = this.tokenCreator.getDefaultExp();
131                 UserTokenPayload data = this.requestUserRoles(response.getAccess_token(), expiresAt);
132                 if (data != null) {
133                     this.handleUserInfoToken(data, resp, host);
134                 } else {
135                     sendErrorResponse(resp, "unable to verify user");
136                 }
137             } else {
138                 this.handleUserInfoToken(response.getAccess_token(), resp, host);
139             }
140         } else {
141             sendErrorResponse(resp, "unable to verify code");
142         }
143     }
144
145     private void handleUserInfoToken(UserTokenPayload data, HttpServletResponse resp, String localHostUrl)
146             throws IOException {
147         BearerToken onapToken = this.tokenCreator.createNewJWT(data);
148         sendTokenResponse(resp, onapToken, localHostUrl);
149     }
150
151     private void handleUserInfoToken(String accessToken, HttpServletResponse resp, String localHostUrl)
152             throws IOException {
153         try {
154             DecodedJWT jwt = JWT.decode(accessToken);
155             String spayload = base64Decode(jwt.getPayload());
156             LOG.debug("payload in jwt='{}'", spayload);
157             UserTokenPayload data = this.mapAccessToken(spayload);
158             this.handleUserInfoToken(data, resp, localHostUrl);
159         } catch (JWTDecodeException | JsonProcessingException e) {
160             LOG.warn("unable to decode jwt token {}: ", accessToken, e);
161             sendErrorResponse(resp, e.getMessage());
162         }
163     }
164
165
166     protected List<String> mapRoles(List<String> roles) {
167         final Map<String, String> map = this.config.getRoleMapping();
168         return roles.stream().map(r -> map.getOrDefault(r, r)).collect(Collectors.toList());
169     }
170
171     private void sendTokenResponse(HttpServletResponse resp, BearerToken data, String localHostUrl) throws IOException {
172         if (this.redirectUri == null) {
173             byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
174             resp.setStatus(200);
175             resp.setContentLength(output.length);
176             resp.setContentType("application/json");
177             ServletOutputStream os = null;
178             os = resp.getOutputStream();
179             os.write(output);
180         } else {
181             resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
182             resp.setHeader("Location", assembleUrl(localHostUrl, this.redirectUri, data.getToken()));
183         }
184     }
185
186
187
188     private static String base64Decode(String data) {
189         return new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8);
190     }
191
192     private OAuthResponseData getTokenForUser(String code, String localHostUrl) {
193
194         Map<String, String> headers = new HashMap<>();
195         headers.put("Content-Type", "application/x-www-form-urlencoded");
196         Map<String, String> params = this.getAdditionalTokenVerifierParams();
197         params.put("code", code);
198         params.put("client_id", this.config.getClientId());
199         params.put("client_secret", this.config.getSecret());
200         params.put("redirect_uri", assembleRedirectUrl(localHostUrl, AuthHttpServlet.REDIRECTURI, this.config.getId()));
201         StringBuilder body = new StringBuilder();
202         for (Entry<String, String> p : params.entrySet()) {
203             body.append(String.format("%s=%s&", p.getKey(), urlEncode(p.getValue())));
204         }
205
206         Optional<MappedBaseHttpResponse<OAuthResponseData>> response =
207                 this.httpClient.sendMappedRequest(this.getTokenVerifierUri(), "POST",
208                         body.substring(0, body.length() - 1), headers, OAuthResponseData.class);
209         if (response.isPresent() && response.get().isSuccess()) {
210             return response.get().body;
211         }
212         LOG.warn("problem get token for code {}", code);
213
214         return null;
215     }
216
217     /**
218      * Assemble callback url for service provider {host}{baseUri}/{serviceId} e.g.
219      * http://10.20.0.11:8181/oauth/redirect/keycloak
220      *
221      * @param host
222      * @param baseUri
223      * @param serviceId
224      * @return
225      */
226     public static String assembleRedirectUrl(String host, String baseUri, String serviceId) {
227         return String.format("%s%s/%s", host, baseUri, serviceId);
228     }
229
230     private static String assembleUrl(String host, String uri, String token) {
231         return String.format("%s%s%s", host, uri, token);
232     }
233
234     public static String urlEncode(String s) {
235         return URLEncoder.encode(s, StandardCharsets.UTF_8);
236     }
237
238     public enum ResponseType {
239         CODE, TOKEN, SESSION_STATE
240     }
241
242
243     public static class PublicOAuthProviderConfig {
244
245         private String id;
246         private String title;
247         private String loginUrl;
248
249         public String getId() {
250             return id;
251         }
252
253         public void setId(String id) {
254             this.id = id;
255         }
256
257         public String getTitle() {
258             return title;
259         }
260
261         public void setTitle(String title) {
262             this.title = title;
263         }
264
265         public String getLoginUrl() {
266             return loginUrl;
267         }
268
269         public void setLoginUrl(String loginUrl) {
270             this.loginUrl = loginUrl;
271         }
272
273         public PublicOAuthProviderConfig(AuthService authService) {
274             this.id = authService.config.getId();
275             this.title = authService.config.getTitle();
276             this.loginUrl = String.format(AuthHttpServlet.LOGIN_REDIRECT_FORMAT, authService.config.getId());
277         }
278
279     }
280
281
282
283 }