add async auth method support
[ccsdk/features.git] / sdnr / wt / oauth-provider / provider-jar / src / main / java / org / onap / ccsdk / features / sdnr / wt / oauthprovider / providers / AuthService.java
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.apache.shiro.authc.BearerToken;
45 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthProviderConfig;
46 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthResponseData;
47 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.UserTokenPayload;
48 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.AuthHttpServlet;
49 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.client.MappedBaseHttpResponse;
50 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.client.MappingBaseHttpClient;
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 issued_at, 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.getUrlOrInternal(), this.config.trustAll());
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                 long issuedAt = this.tokenCreator.getDefaultIat();
132                 UserTokenPayload data = this.requestUserRoles(response.getAccess_token(), issuedAt, expiresAt);
133                 if (data != null) {
134                     this.handleUserInfoToken(data, resp, host);
135                 } else {
136                     sendErrorResponse(resp, "unable to verify user");
137                 }
138             } else {
139                 this.handleUserInfoToken(response.getAccess_token(), resp, host);
140             }
141         } else {
142             sendErrorResponse(resp, "unable to verify code");
143         }
144     }
145
146     private void handleUserInfoToken(UserTokenPayload data, HttpServletResponse resp, String localHostUrl)
147             throws IOException {
148         BearerToken onapToken = this.tokenCreator.createNewJWT(data);
149         sendTokenResponse(resp, onapToken, localHostUrl);
150     }
151
152     private void handleUserInfoToken(String accessToken, HttpServletResponse resp, String localHostUrl)
153             throws IOException {
154         try {
155             DecodedJWT jwt = JWT.decode(accessToken);
156             String spayload = base64Decode(jwt.getPayload());
157             LOG.debug("payload in jwt='{}'", spayload);
158             UserTokenPayload data = this.mapAccessToken(spayload);
159             this.handleUserInfoToken(data, resp, localHostUrl);
160         } catch (JWTDecodeException | JsonProcessingException e) {
161             LOG.warn("unable to decode jwt token {}: ", accessToken, e);
162             sendErrorResponse(resp, e.getMessage());
163         }
164     }
165
166
167     protected List<String> mapRoles(List<String> roles) {
168         final Map<String, String> map = this.config.getRoleMapping();
169         return roles.stream().map(r -> map.getOrDefault(r, r)).collect(Collectors.toList());
170     }
171
172     private void sendTokenResponse(HttpServletResponse resp, BearerToken data, String localHostUrl) throws IOException {
173         if (this.redirectUri == null) {
174             byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
175             resp.setStatus(200);
176             resp.setContentLength(output.length);
177             resp.setContentType("application/json");
178             ServletOutputStream os = null;
179             os = resp.getOutputStream();
180             os.write(output);
181         } else {
182             resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
183             resp.setHeader("Location", assembleUrl(localHostUrl, this.redirectUri, data.getToken()));
184         }
185     }
186
187
188
189     private static String base64Decode(String data) {
190         return new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8);
191     }
192
193     private OAuthResponseData getTokenForUser(String code, String localHostUrl) {
194
195         Map<String, String> headers = new HashMap<>();
196         headers.put("Content-Type", "application/x-www-form-urlencoded");
197         Map<String, String> params = this.getAdditionalTokenVerifierParams();
198         params.put("code", code);
199         params.put("client_id", this.config.getClientId());
200         params.put("client_secret", this.config.getSecret());
201         params.put("redirect_uri", assembleRedirectUrl(localHostUrl, AuthHttpServlet.REDIRECTURI, this.config.getId()));
202         StringBuilder body = new StringBuilder();
203         for (Entry<String, String> p : params.entrySet()) {
204             body.append(String.format("%s=%s&", p.getKey(), urlEncode(p.getValue())));
205         }
206
207         Optional<MappedBaseHttpResponse<OAuthResponseData>> response =
208                 this.httpClient.sendMappedRequest(this.getTokenVerifierUri(), "POST",
209                         body.substring(0, body.length() - 1), headers, OAuthResponseData.class);
210         if (response.isPresent() && response.get().isSuccess()) {
211             return response.get().body;
212         }
213         LOG.warn("problem get token for code {}", code);
214
215         return null;
216     }
217
218     /**
219      * Assemble callback url for service provider {host}{baseUri}/{serviceId} e.g.
220      * http://10.20.0.11:8181/oauth/redirect/keycloak
221      *
222      * @param host
223      * @param baseUri
224      * @param serviceId
225      * @return
226      */
227     public static String assembleRedirectUrl(String host, String baseUri, String serviceId) {
228         return String.format("%s%s/%s", host, baseUri, serviceId);
229     }
230
231     private static String assembleUrl(String host, String uri, String token) {
232         return String.format("%s%s%s", host, uri, token);
233     }
234
235     public static String urlEncode(String s) {
236         return URLEncoder.encode(s, StandardCharsets.UTF_8);
237     }
238
239     public enum ResponseType {
240         CODE, TOKEN, SESSION_STATE
241     }
242
243
244     public static class PublicOAuthProviderConfig {
245
246         private String id;
247         private String title;
248         private String loginUrl;
249
250         public String getId() {
251             return id;
252         }
253
254         public void setId(String id) {
255             this.id = id;
256         }
257
258         public String getTitle() {
259             return title;
260         }
261
262         public void setTitle(String title) {
263             this.title = title;
264         }
265
266         public String getLoginUrl() {
267             return loginUrl;
268         }
269
270         public void setLoginUrl(String loginUrl) {
271             this.loginUrl = loginUrl;
272         }
273
274         public PublicOAuthProviderConfig(AuthService authService) {
275             this.id = authService.config.getId();
276             this.title = authService.config.getTitle();
277             this.loginUrl = String.format(AuthHttpServlet.LOGIN_REDIRECT_FORMAT, authService.config.getId());
278         }
279
280     }
281
282
283
284 }