aboutsummaryrefslogtreecommitdiffstats
path: root/sdnr/wt/oauth-provider/oauth-core/src/main/java/org/onap/ccsdk/features/sdnr/wt/oauthprovider/providers/AuthService.java
blob: 2dc0b57466245b358d9aba184bc01a1f905d7ec0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
/*
 * ============LICENSE_START=======================================================
 * ONAP : ccsdk features
 * ================================================================================
 * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property.
 * All rights reserved.
 * ================================================================================
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ============LICENSE_END=========================================================
 *
 */
package org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers;

import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.*;
import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.AuthHttpServlet;
import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.client.MappedBaseHttpResponse;
import org.onap.ccsdk.features.sdnr.wt.oauthprovider.http.client.MappingBaseHttpClient;
import org.apache.shiro.authc.BearerToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AuthService {


    private static final Logger LOG = LoggerFactory.getLogger(AuthService.class);
    private final MappingBaseHttpClient httpClient;
    protected final ObjectMapper mapper;
    protected final OAuthProviderConfig config;
    protected final TokenCreator tokenCreator;
    private final String redirectUri;
    private final String tokenEndpointRelative;
    private final String authEndpointAbsolute;
    private final String logoutEndpointAbsolute;

    private final Map<String, String> logoutTokenMap;
    protected abstract String getTokenVerifierUri();

    protected abstract Map<String, String> getAdditionalTokenVerifierParams();

    protected abstract ResponseType getResponseType();

    protected abstract boolean doSeperateRolesRequest();

    protected abstract UserTokenPayload mapAccessToken(String spayload)
            throws JsonMappingException, JsonProcessingException;

    protected abstract String getLoginUrl(String callbackUrl);
    protected abstract String getLogoutUrl();

    protected abstract UserTokenPayload requestUserRoles(String access_token, long issued_at, long expires_at);

    protected abstract boolean verifyState(String state);

    public AuthService(OAuthProviderConfig config, String redirectUri, TokenCreator tokenCreator) throws UnableToConfigureOAuthService {
        this.config = config;
        this.tokenCreator = tokenCreator;
        this.redirectUri = redirectUri;
        this.mapper = new ObjectMapper();
        this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        this.httpClient = new MappingBaseHttpClient(this.config.getUrlOrInternal(), this.config.trustAll());
        this.logoutTokenMap = new HashMap<>();
        if (this.config.hasToBeConfigured()){
            Optional<MappedBaseHttpResponse<OpenIdConfigResponseData>> oresponse = this.httpClient.sendMappedRequest(
                    this.config.getOpenIdConfigUrl(), "GET", null, null, OpenIdConfigResponseData.class);
            if(oresponse.isEmpty()){
                throw new UnableToConfigureOAuthService(this.config.getOpenIdConfigUrl());
            }
            MappedBaseHttpResponse<OpenIdConfigResponseData> response = oresponse.get();
            if(!response.isSuccess()){
                throw new UnableToConfigureOAuthService(this.config.getOpenIdConfigUrl(), response.code);
            }
            this.tokenEndpointRelative = trimUrl(this.config.getUrlOrInternal(),response.body.getToken_endpoint());
            this.authEndpointAbsolute = extendUrl(this.config.getUrlOrInternal(),response.body.getAuthorization_endpoint());
            this.logoutEndpointAbsolute = extendUrl(this.config.getUrlOrInternal(),response.body.getEnd_session_endpoint());
        }
        else{
            this.tokenEndpointRelative = null;
            this.authEndpointAbsolute = null;
            this.logoutEndpointAbsolute = null;
        }
    }

    public static String trimUrl(String baseUrl, String endpoint) {
        if(endpoint.startsWith(baseUrl)){
            return endpoint.substring(baseUrl.length());
        }
        if(endpoint.startsWith("http")){
            return endpoint.substring(endpoint.indexOf("/",8));
        }
        return endpoint;
    }
    public static String extendUrl(String baseUrl, String endpoint) {
        if(endpoint.startsWith("http")){
            endpoint= endpoint.substring(endpoint.indexOf("/",8));
        }
        if(baseUrl.endsWith("/")){
            baseUrl=baseUrl.substring(0,baseUrl.length()-2);
        }
        return baseUrl+endpoint;
    }

    public PublicOAuthProviderConfig getConfig() {
        return new PublicOAuthProviderConfig(this);
    }

    protected MappingBaseHttpClient getHttpClient() {
        return this.httpClient;
    }

    public void handleRedirect(HttpServletRequest req, HttpServletResponse resp, String host) throws IOException {
        switch (this.getResponseType()) {
            case CODE:
                this.handleRedirectCode(req, resp, host);
                break;
            case TOKEN:
                sendErrorResponse(resp, "not yet implemented");
                break;
            case SESSION_STATE:
                break;
        }
    }

    public void sendLoginRedirectResponse(HttpServletResponse resp, String callbackUrl) {
        resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
        String url = this.authEndpointAbsolute !=null?String.format(
                "%s?client_id=%s&response_type=code&scope=%s&redirect_uri=%s",
                this.authEndpointAbsolute, urlEncode(this.config.getClientId()), this.config.getScope(),
                urlEncode(callbackUrl)):this.getLoginUrl(callbackUrl);
        resp.setHeader("Location", url);
    }
    public void sendLogoutRedirectResponse(String token, HttpServletResponse resp, String redirectUrl)
            throws IOException {
        String idToken = this.logoutTokenMap.getOrDefault(token, null);
        String logoutEndpoint = this.logoutEndpointAbsolute!=null?this.logoutEndpointAbsolute:this.getLogoutUrl();
        if(idToken==null) {
            LOG.debug("unable to find token in map. Do unsafe logout.");
            resp.sendRedirect(this.logoutEndpointAbsolute);
            return;
        }
        LOG.debug("id token found. redirect to specific logout");
        resp.sendRedirect(String.format("%s?id_token_hint=%s&post_logout_redirect_uri=%s",logoutEndpoint, idToken,
                urlEncode(redirectUrl)));
    }



    private static void sendErrorResponse(HttpServletResponse resp, String message) throws IOException {
        resp.sendError(HttpServletResponse.SC_NOT_FOUND, message);
    }

    private void handleRedirectCode(HttpServletRequest req, HttpServletResponse resp, String host) throws IOException {
        final String code = req.getParameter("code");
        final String state = req.getParameter("state");
        OAuthResponseData response = null;
        if(this.verifyState(state)) {
            response = this.getTokenForUser(code, host);
        }
        if (response != null) {
            if (this.doSeperateRolesRequest()) {
                LOG.debug("do a seperate role request");
                long expiresAt = this.tokenCreator.getDefaultExp();
                long issuedAt = this.tokenCreator.getDefaultIat();
                UserTokenPayload data = this.requestUserRoles(response.getAccess_token(), issuedAt, expiresAt);
                if (data != null) {
                    BearerToken createdToken = this.handleUserInfoToken(data, resp, host);
                    this.logoutTokenMap.put(createdToken.getToken(),response.getId_token());
                } else {
                    sendErrorResponse(resp, "unable to verify user");
                }
            } else {
                BearerToken createdToken = this.handleUserInfoToken(response.getAccess_token(), resp, host);
                this.logoutTokenMap.put(createdToken.getToken(),response.getId_token());
            }
        } else {
            sendErrorResponse(resp, "unable to verify code");
        }
    }

    private BearerToken handleUserInfoToken(UserTokenPayload data, HttpServletResponse resp, String localHostUrl)
            throws IOException {
        BearerToken onapToken = this.tokenCreator.createNewJWT(data);
        sendTokenResponse(resp, onapToken, localHostUrl);
        return onapToken;
    }

    private BearerToken handleUserInfoToken(String accessToken, HttpServletResponse resp, String localHostUrl)
            throws IOException {
        try {
            DecodedJWT jwt = JWT.decode(accessToken);
            String spayload = base64Decode(jwt.getPayload());
            LOG.debug("payload in jwt='{}'", spayload);
            UserTokenPayload data = this.mapAccessToken(spayload);
            return this.handleUserInfoToken(data, resp, localHostUrl);
        } catch (JWTDecodeException | JsonProcessingException e) {
            LOG.warn("unable to decode jwt token {}: ", accessToken, e);
            sendErrorResponse(resp, e.getMessage());
        }
        return null;
    }


    protected List<String> mapRoles(List<String> roles) {
        final Map<String, String> map = this.config.getRoleMapping();
        return roles.stream().map(r -> map.getOrDefault(r, r)).collect(Collectors.toList());
    }

    private void sendTokenResponse(HttpServletResponse resp, BearerToken data, String localHostUrl) throws IOException {
        if (this.redirectUri == null) {
            byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
            resp.setStatus(200);
            resp.setContentLength(output.length);
            resp.setContentType("application/json");
            resp.addCookie(this.tokenCreator.createAuthCookie(data));
            ServletOutputStream os = null;
            os = resp.getOutputStream();
            os.write(output);
        } else {
            resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
            resp.setHeader("Location", assembleUrl(localHostUrl, this.redirectUri, data.getToken()));
            resp.addCookie(this.tokenCreator.createAuthCookie(data));
        }
    }



    private static String base64Decode(String data) {
        return new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8);
    }

    private OAuthResponseData getTokenForUser(String code, String localHostUrl) {

        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/x-www-form-urlencoded");
        headers.put("Accept", "application/json");
        Map<String, String> params = this.getAdditionalTokenVerifierParams();
        params.put("code", code);
        params.put("client_id", this.config.getClientId());
        params.put("client_secret", this.config.getSecret());
        params.put("redirect_uri", assembleRedirectUrl(localHostUrl, AuthHttpServlet.REDIRECTURI, this.config.getId()));
        StringBuilder body = new StringBuilder();
        for (Entry<String, String> p : params.entrySet()) {
            body.append(String.format("%s=%s&", p.getKey(), urlEncode(p.getValue())));
        }

        String url = this.tokenEndpointRelative !=null?this.tokenEndpointRelative :this.getTokenVerifierUri();
        Optional<MappedBaseHttpResponse<OAuthResponseData>> response =
                this.httpClient.sendMappedRequest(url, "POST",
                        body.substring(0, body.length() - 1), headers, OAuthResponseData.class);
        if (response.isPresent() && response.get().isSuccess()) {
            return response.get().body;
        }
        LOG.warn("problem get token for code {}", code);

        return null;
    }

    /**
     * Assemble callback url for service provider {host}{baseUri}/{serviceId} e.g.
     * http://10.20.0.11:8181/oauth/redirect/keycloak
     *
     * @param host
     * @param baseUri
     * @param serviceId
     * @return
     */
    public static String assembleRedirectUrl(String host, String baseUri, String serviceId) {
        return String.format("%s%s/%s", host, baseUri, serviceId);
    }

    private static String assembleUrl(String host, String uri, String token) {
        return String.format("%s%s%s", host, uri, token);
    }

    public static String urlEncode(String s) {
        return URLEncoder.encode(s, StandardCharsets.UTF_8);
    }



    public enum ResponseType {
        CODE, TOKEN, SESSION_STATE
    }


    public static class PublicOAuthProviderConfig {

        private String id;
        private String title;
        private String loginUrl;

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getLoginUrl() {
            return loginUrl;
        }

        public void setLoginUrl(String loginUrl) {
            this.loginUrl = loginUrl;
        }

        public PublicOAuthProviderConfig(AuthService authService) {
            this.id = authService.config.getId();
            this.title = authService.config.getTitle();
            this.loginUrl = String.format(AuthHttpServlet.LOGIN_REDIRECT_FORMAT, authService.config.getId());
        }

    }



}