2 * ============LICENSE_START=======================================================
3 * ONAP : ccsdk features
4 * ================================================================================
5 * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property.
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
12 * http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
22 package org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers;
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;
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;
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;
53 public abstract class AuthService {
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 tokenEndpointRelative;
63 private final String authEndpointAbsolute;
64 private final String logoutEndpointAbsolute;
66 private final Map<String, String> logoutTokenMap;
67 protected abstract String getTokenVerifierUri();
69 protected abstract Map<String, String> getAdditionalTokenVerifierParams();
71 protected abstract ResponseType getResponseType();
73 protected abstract boolean doSeperateRolesRequest();
75 protected abstract UserTokenPayload mapAccessToken(String spayload)
76 throws JsonMappingException, JsonProcessingException;
78 protected abstract String getLoginUrl(String callbackUrl);
79 protected abstract String getLogoutUrl();
81 protected abstract UserTokenPayload requestUserRoles(String access_token, long issued_at, long expires_at);
83 protected abstract boolean verifyState(String state);
85 public AuthService(OAuthProviderConfig config, String redirectUri, TokenCreator tokenCreator) throws UnableToConfigureOAuthService {
87 this.tokenCreator = tokenCreator;
88 this.redirectUri = redirectUri;
89 this.mapper = new ObjectMapper();
90 this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
91 this.httpClient = new MappingBaseHttpClient(this.config.getUrlOrInternal(), this.config.trustAll());
92 this.logoutTokenMap = new HashMap<>();
93 if (this.config.hasToBeConfigured()){
94 Optional<MappedBaseHttpResponse<OpenIdConfigResponseData>> oresponse = this.httpClient.sendMappedRequest(
95 this.config.getOpenIdConfigUrl(), "GET", null, null, OpenIdConfigResponseData.class);
96 if(oresponse.isEmpty()){
97 throw new UnableToConfigureOAuthService(this.config.getOpenIdConfigUrl());
99 MappedBaseHttpResponse<OpenIdConfigResponseData> response = oresponse.get();
100 if(!response.isSuccess()){
101 throw new UnableToConfigureOAuthService(this.config.getOpenIdConfigUrl(), response.code);
103 this.tokenEndpointRelative = trimUrl(this.config.getUrlOrInternal(),response.body.getToken_endpoint());
104 this.authEndpointAbsolute = extendUrl(this.config.getUrlOrInternal(),response.body.getAuthorization_endpoint());
105 this.logoutEndpointAbsolute = extendUrl(this.config.getUrlOrInternal(),response.body.getEnd_session_endpoint());
108 this.tokenEndpointRelative = null;
109 this.authEndpointAbsolute = null;
110 this.logoutEndpointAbsolute = null;
114 public static String trimUrl(String baseUrl, String endpoint) {
115 if(endpoint.startsWith(baseUrl)){
116 return endpoint.substring(baseUrl.length());
118 if(endpoint.startsWith("http")){
119 return endpoint.substring(endpoint.indexOf("/",8));
123 public static String extendUrl(String baseUrl, String endpoint) {
124 if(endpoint.startsWith("http")){
125 endpoint= endpoint.substring(endpoint.indexOf("/",8));
127 if(baseUrl.endsWith("/")){
128 baseUrl=baseUrl.substring(0,baseUrl.length()-2);
130 return baseUrl+endpoint;
133 public PublicOAuthProviderConfig getConfig() {
134 return new PublicOAuthProviderConfig(this);
137 protected MappingBaseHttpClient getHttpClient() {
138 return this.httpClient;
141 public void handleRedirect(HttpServletRequest req, HttpServletResponse resp, String host) throws IOException {
142 switch (this.getResponseType()) {
144 this.handleRedirectCode(req, resp, host);
147 sendErrorResponse(resp, "not yet implemented");
154 public void sendLoginRedirectResponse(HttpServletResponse resp, String callbackUrl) {
155 resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
156 String url = this.authEndpointAbsolute !=null?String.format(
157 "%s?client_id=%s&response_type=code&scope=%s&redirect_uri=%s",
158 this.authEndpointAbsolute, urlEncode(this.config.getClientId()), this.config.getScope(),
159 urlEncode(callbackUrl)):this.getLoginUrl(callbackUrl);
160 resp.setHeader("Location", url);
162 public void sendLogoutRedirectResponse(String token, HttpServletResponse resp, String redirectUrl)
164 String idToken = this.logoutTokenMap.getOrDefault(token, null);
165 String logoutEndpoint = this.logoutEndpointAbsolute!=null?this.logoutEndpointAbsolute:this.getLogoutUrl();
167 LOG.debug("unable to find token in map. Do unsafe logout.");
168 resp.sendRedirect(this.logoutEndpointAbsolute);
171 LOG.debug("id token found. redirect to specific logout");
172 resp.sendRedirect(String.format("%s?id_token_hint=%s&post_logout_redirect_uri=%s",logoutEndpoint, idToken,
173 urlEncode(redirectUrl)));
178 private static void sendErrorResponse(HttpServletResponse resp, String message) throws IOException {
179 resp.sendError(HttpServletResponse.SC_NOT_FOUND, message);
182 private void handleRedirectCode(HttpServletRequest req, HttpServletResponse resp, String host) throws IOException {
183 final String code = req.getParameter("code");
184 final String state = req.getParameter("state");
185 OAuthResponseData response = null;
186 if(this.verifyState(state)) {
187 response = this.getTokenForUser(code, host);
189 if (response != null) {
190 if (this.doSeperateRolesRequest()) {
191 LOG.debug("do a seperate role request");
192 long expiresAt = this.tokenCreator.getDefaultExp();
193 long issuedAt = this.tokenCreator.getDefaultIat();
194 UserTokenPayload data = this.requestUserRoles(response.getAccess_token(), issuedAt, expiresAt);
196 BearerToken createdToken = this.handleUserInfoToken(data, resp, host);
197 this.logoutTokenMap.put(createdToken.getToken(),response.getId_token());
199 sendErrorResponse(resp, "unable to verify user");
202 BearerToken createdToken = this.handleUserInfoToken(response.getAccess_token(), resp, host);
203 this.logoutTokenMap.put(createdToken.getToken(),response.getId_token());
206 sendErrorResponse(resp, "unable to verify code");
210 private BearerToken handleUserInfoToken(UserTokenPayload data, HttpServletResponse resp, String localHostUrl)
212 BearerToken onapToken = this.tokenCreator.createNewJWT(data);
213 sendTokenResponse(resp, onapToken, localHostUrl);
217 private BearerToken handleUserInfoToken(String accessToken, HttpServletResponse resp, String localHostUrl)
220 DecodedJWT jwt = JWT.decode(accessToken);
221 String spayload = base64Decode(jwt.getPayload());
222 LOG.debug("payload in jwt='{}'", spayload);
223 UserTokenPayload data = this.mapAccessToken(spayload);
224 return this.handleUserInfoToken(data, resp, localHostUrl);
225 } catch (JWTDecodeException | JsonProcessingException e) {
226 LOG.warn("unable to decode jwt token {}: ", accessToken, e);
227 sendErrorResponse(resp, e.getMessage());
233 protected List<String> mapRoles(List<String> roles) {
234 final Map<String, String> map = this.config.getRoleMapping();
235 return roles.stream().map(r -> map.getOrDefault(r, r)).collect(Collectors.toList());
238 private void sendTokenResponse(HttpServletResponse resp, BearerToken data, String localHostUrl) throws IOException {
239 if (this.redirectUri == null) {
240 byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
242 resp.setContentLength(output.length);
243 resp.setContentType("application/json");
244 resp.addCookie(this.tokenCreator.createAuthCookie(data));
245 ServletOutputStream os = null;
246 os = resp.getOutputStream();
249 resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
250 resp.setHeader("Location", assembleUrl(localHostUrl, this.redirectUri, data.getToken()));
251 resp.addCookie(this.tokenCreator.createAuthCookie(data));
257 private static String base64Decode(String data) {
258 return new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8);
261 private OAuthResponseData getTokenForUser(String code, String localHostUrl) {
263 Map<String, String> headers = new HashMap<>();
264 headers.put("Content-Type", "application/x-www-form-urlencoded");
265 headers.put("Accept", "application/json");
266 Map<String, String> params = this.getAdditionalTokenVerifierParams();
267 params.put("code", code);
268 params.put("client_id", this.config.getClientId());
269 params.put("client_secret", this.config.getSecret());
270 params.put("redirect_uri", assembleRedirectUrl(localHostUrl, AuthHttpServlet.REDIRECTURI, this.config.getId()));
271 StringBuilder body = new StringBuilder();
272 for (Entry<String, String> p : params.entrySet()) {
273 body.append(String.format("%s=%s&", p.getKey(), urlEncode(p.getValue())));
276 String url = this.tokenEndpointRelative !=null?this.tokenEndpointRelative :this.getTokenVerifierUri();
277 Optional<MappedBaseHttpResponse<OAuthResponseData>> response =
278 this.httpClient.sendMappedRequest(url, "POST",
279 body.substring(0, body.length() - 1), headers, OAuthResponseData.class);
280 if (response.isPresent() && response.get().isSuccess()) {
281 return response.get().body;
283 LOG.warn("problem get token for code {}", code);
289 * Assemble callback url for service provider {host}{baseUri}/{serviceId} e.g.
290 * http://10.20.0.11:8181/oauth/redirect/keycloak
297 public static String assembleRedirectUrl(String host, String baseUri, String serviceId) {
298 return String.format("%s%s/%s", host, baseUri, serviceId);
301 private static String assembleUrl(String host, String uri, String token) {
302 return String.format("%s%s%s", host, uri, token);
305 public static String urlEncode(String s) {
306 return URLEncoder.encode(s, StandardCharsets.UTF_8);
311 public enum ResponseType {
312 CODE, TOKEN, SESSION_STATE
316 public static class PublicOAuthProviderConfig {
319 private String title;
320 private String loginUrl;
322 public String getId() {
326 public void setId(String id) {
330 public String getTitle() {
334 public void setTitle(String title) {
338 public String getLoginUrl() {
342 public void setLoginUrl(String loginUrl) {
343 this.loginUrl = loginUrl;
346 public PublicOAuthProviderConfig(AuthService authService) {
347 this.id = authService.config.getId();
348 this.title = authService.config.getTitle();
349 this.loginUrl = String.format(AuthHttpServlet.LOGIN_REDIRECT_FORMAT, authService.config.getId());