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.http;
24 import com.fasterxml.jackson.databind.ObjectMapper;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.HashMap;
30 import java.util.List;
32 import java.util.Optional;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import javax.servlet.ServletException;
36 import javax.servlet.ServletOutputStream;
37 import javax.servlet.http.HttpServlet;
38 import javax.servlet.http.HttpServletRequest;
39 import javax.servlet.http.HttpServletResponse;
40 import org.apache.shiro.SecurityUtils;
41 import org.apache.shiro.ShiroException;
42 import org.apache.shiro.codec.Base64;
43 import org.apache.shiro.session.Session;
44 import org.apache.shiro.subject.Subject;
45 import org.jolokia.osgi.security.Authenticator;
46 import org.onap.ccsdk.features.sdnr.wt.common.http.BaseHTTPClient;
47 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.Config;
48 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.NoDefinitionFoundException;
49 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthProviderConfig;
50 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OAuthToken;
51 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OdlPolicy;
52 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.UserTokenPayload;
53 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService;
54 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService.PublicOAuthProviderConfig;
55 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.MdSalAuthorizationStore;
56 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.OAuthProviderFactory;
57 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.TokenCreator;
58 import org.opendaylight.aaa.api.IdMService;
59 import org.opendaylight.aaa.shiro.filters.backport.BearerToken;
60 import org.opendaylight.mdsal.binding.api.DataBroker;
61 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.ShiroConfiguration;
62 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.shiro.configuration.Main;
63 import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.app.config.rev170619.shiro.configuration.Urls;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
67 public class AuthHttpServlet extends HttpServlet {
69 private static final Logger LOG = LoggerFactory.getLogger(AuthHttpServlet.class.getName());
70 private static final long serialVersionUID = 1L;
71 private static final String BASEURI = "/oauth";
72 private static final String LOGINURI = BASEURI + "/login";
73 private static final String LOGOUTURI = BASEURI + "/logout";
74 private static final String PROVIDERSURI = BASEURI + "/providers";
75 public static final String REDIRECTURI = BASEURI + "/redirect";
76 private static final String REDIRECTURI_FORMAT = REDIRECTURI + "/%s";
77 private static final String POLICIESURI = BASEURI + "/policies";
78 //private static final String PROVIDERID_REGEX = "^\\" + BASEURI + "\\/providers\\/([^\\/]+)$";
79 private static final String REDIRECTID_REGEX = "^\\" + BASEURI + "\\/redirect\\/([^\\/]+)$";
80 private static final String LOGIN_REDIRECT_REGEX = "^\\" + LOGINURI + "\\/([^\\/]+)$";
81 //private static final Pattern PROVIDERID_PATTERN = Pattern.compile(PROVIDERID_REGEX);
82 private static final Pattern REDIRECTID_PATTERN = Pattern.compile(REDIRECTID_REGEX);
83 private static final Pattern LOGIN_REDIRECT_PATTERN = Pattern.compile(LOGIN_REDIRECT_REGEX);
85 private static final String DEFAULT_DOMAIN = "sdn";
86 private static final String HEAEDER_AUTHORIZATION = "Authorization";
88 private static final String CLASSNAME_ODLBASICAUTH =
89 "org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter";
90 private static final String CLASSNAME_ODLBEARERANDBASICAUTH =
91 "org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter2";
92 private static final String CLASSNAME_ODLMDSALAUTH =
93 "org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter";
94 public static final String LOGIN_REDIRECT_FORMAT = LOGINURI + "/%s";
96 private final ObjectMapper mapper;
97 /* state <=> AuthProviderService> */
98 private final Map<String, AuthService> providerStore;
99 private Authenticator odlAuthenticator;
100 private IdMService odlIdentityService;
101 private final TokenCreator tokenCreator;
102 private final Config config;
103 private ShiroConfiguration shiroConfiguration;
104 private DataBroker dataBroker;
105 private MdSalAuthorizationStore mdsalAuthStore;
107 public AuthHttpServlet() throws IOException {
108 this.config = Config.getInstance();
109 this.tokenCreator = TokenCreator.getInstance(this.config);
110 this.mapper = new ObjectMapper();
111 this.providerStore = new HashMap<>();
112 for (OAuthProviderConfig pc : config.getProviders()) {
113 this.providerStore.put(pc.getId(), OAuthProviderFactory.create(pc.getType(), pc,
114 this.config.getRedirectUri(), TokenCreator.getInstance(this.config)));
119 public void setOdlAuthenticator(Authenticator odlAuthenticator) {
120 this.odlAuthenticator = odlAuthenticator;
123 public void setOdlIdentityService(IdMService odlIdentityService) {
124 this.odlIdentityService = odlIdentityService;
127 public void setShiroConfiguration(ShiroConfiguration shiroConfiguration) {
128 this.shiroConfiguration = shiroConfiguration;
131 public void setDataBroker(DataBroker dataBroker) {
132 this.dataBroker = dataBroker;
133 this.mdsalAuthStore = new MdSalAuthorizationStore(this.dataBroker);
137 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
138 LOG.debug("GET request for {}", req.getRequestURI());
140 if (PROVIDERSURI.equals(req.getRequestURI())) {
141 this.sendResponse(resp, HttpServletResponse.SC_OK, getConfigs(this.providerStore.values()));
142 } else if (req.getRequestURI().startsWith(LOGINURI)) {
143 this.handleLoginRedirect(req, resp);
144 } else if (req.getRequestURI().equals(LOGOUTURI)) {
145 this.handleLogout(req, resp);
146 } else if (POLICIESURI.equals(req.getRequestURI())) {
147 this.sendResponse(resp, HttpServletResponse.SC_OK, this.getPoliciesForUser(req));
148 } else if (req.getRequestURI().startsWith(REDIRECTURI)) {
149 this.handleRedirect(req, resp);
151 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
155 private void handleLogout(HttpServletRequest req, HttpServletResponse resp) throws IOException {
157 this.sendResponse(resp, HttpServletResponse.SC_OK,"");
159 private void handleLoginRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
160 final String uri = req.getRequestURI();
161 final Matcher matcher = LOGIN_REDIRECT_PATTERN.matcher(uri);
162 if (matcher.find()) {
163 final String id = matcher.group(1);
164 AuthService provider = this.providerStore.getOrDefault(id, null);
165 if (provider != null) {
166 //provider.setLocalHostUrl(getHost(req));
167 String redirectUrl = getHost(req) + String.format(REDIRECTURI_FORMAT, id);
168 provider.sendLoginRedirectResponse(resp, redirectUrl);
172 this.sendResponse(resp, HttpServletResponse.SC_NOT_FOUND, "");
176 * find out what urls can be accessed by user and which are forbidden
178 * urlEntries: "anon" -> any access allowed "authcXXX" -> no grouping rule -> any access for user allowed "authcXXX,
179 * roles[abc] -> user needs to have role abc "authcXXX, roles["abc,def"] -> user needs to have roles abc AND def
180 * "authcXXX, anyroles[abc] -> user needs to have role abc "authcXXX, anyroles["abc,def"] -> user needs to have
187 private List<OdlPolicy> getPoliciesForUser(HttpServletRequest req) {
188 List<Urls> urlRules = this.shiroConfiguration.getUrls();
189 UserTokenPayload data = this.getUserInfo(req);
190 List<OdlPolicy> policies = new ArrayList<>();
191 if (urlRules != null) {
192 LOG.debug("try to find rules for user {} with roles {}",
193 data == null ? "null" : data.getPreferredUsername(), data == null ? "null" : data.getRoles());
194 final String regex = "^([^,]+)[,]?[\\ ]?([anyroles]+)?(\\[\"?([a-zA-Z,]+)\"?\\])?";
195 final Pattern pattern = Pattern.compile(regex);
197 for (Urls urlRule : urlRules) {
198 matcher = pattern.matcher(urlRule.getPairValue());
199 if (matcher.find()) {
201 final String authClass = getAuthClass(matcher.group(1));
202 Optional<OdlPolicy> policy = Optional.empty();
203 //anon access allowed
204 if (authClass == null) {
205 policy = Optional.of(OdlPolicy.allowAll(urlRule.getPairKey()));
206 } else if (authClass.equals(CLASSNAME_ODLBASICAUTH)) {
207 policy = isBasic(req) ? this.getTokenBasedPolicy(urlRule, matcher, data)
208 : Optional.of(OdlPolicy.denyAll(urlRule.getPairKey()));
209 } else if (authClass.equals(CLASSNAME_ODLBEARERANDBASICAUTH)) {
210 policy = this.getTokenBasedPolicy(urlRule, matcher, data);
211 } else if (authClass.equals(CLASSNAME_ODLMDSALAUTH)) {
212 policy = this.getMdSalBasedPolicy(urlRule, matcher, data);
214 if (policy.isPresent()) {
215 policies.add(policy.get());
217 LOG.warn("unable to get policy for authClass {} for entry {}", authClass,
218 urlRule.getPairValue());
219 policies.add(OdlPolicy.denyAll(urlRule.getPairKey()));
221 } catch (NoDefinitionFoundException e) {
222 LOG.warn("unknown authClass: ", e);
226 LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
230 LOG.debug("no url rules found");
236 * extract policy rule for user from MD-SAL not yet supported
243 private Optional<OdlPolicy> getMdSalBasedPolicy(Urls urlRule, Matcher matcher, UserTokenPayload data) {
244 if (this.mdsalAuthStore != null) {
245 return data != null ? this.mdsalAuthStore.getPolicy(urlRule.getPairKey(), data.getRoles())
246 : Optional.of(OdlPolicy.denyAll(urlRule.getPairKey()));
248 return Optional.empty();
252 * extract policy rule for user from url rules of config
259 private Optional<OdlPolicy> getTokenBasedPolicy(Urls urlRule, Matcher matcher, UserTokenPayload data) {
260 final String url = urlRule.getPairKey();
261 final String rule = urlRule.getPairValue();
262 if (!rule.contains(",")) {
263 LOG.debug("found rule without roles for '{}'", matcher.group(1));
264 //not important if anon or authcXXX
265 if (data != null || "anon".equals(matcher.group(1))) {
266 return Optional.of(OdlPolicy.allowAll(url));
270 LOG.debug("found rule with roles '{}'", matcher.group(4));
271 if ("roles".equals(matcher.group(2))) {
272 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), false)) {
273 return Optional.of(OdlPolicy.allowAll(url));
275 return Optional.of(OdlPolicy.denyAll(url));
277 } else if ("anyroles".equals(matcher.group(2))) {
278 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), true)) {
279 return Optional.of(OdlPolicy.allowAll(url));
281 return Optional.of(OdlPolicy.denyAll(url));
284 LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
287 return Optional.of(OdlPolicy.denyAll(url));
289 return Optional.empty();
292 private String getAuthClass(String key) throws NoDefinitionFoundException {
293 if ("anon".equals(key)) {
296 Optional<Main> main =
297 this.shiroConfiguration.getMain().stream().filter((e) -> e.getPairKey().equals(key)).findFirst();
298 if (main.isPresent()) {
299 return main.get().getPairValue();
301 throw new NoDefinitionFoundException("unable to find def for " + key);
304 private UserTokenPayload getUserInfo(HttpServletRequest req) {
306 UserTokenPayload data = TokenCreator.getInstance(this.config).decode(req);
310 } else if (isBasic(req)) {
311 String username = getBasicAuthUsername(req);
312 if (username != null) {
313 final String domain = getBasicAuthDomain(username);
314 if (!username.contains("@")) {
315 username = String.format("%s@%s", username, domain);
317 List<String> roles = this.odlIdentityService.listRoles(username, domain);
318 return UserTokenPayload.create(username, roles);
324 private static String getBasicAuthDomain(String username) {
325 if (username.contains("@")) {
326 return username.split("@")[1];
328 return DEFAULT_DOMAIN;
331 private static String getBasicAuthUsername(HttpServletRequest req) {
332 final String header = req.getHeader(HEAEDER_AUTHORIZATION);
333 final String decoded = Base64.decodeToString(header.substring(6));
334 // attempt to decode username/password; otherwise decode as token
335 if (decoded.contains(":")) {
336 return decoded.split(":")[0];
338 LOG.warn("unable to detect username from basicauth header {}", header);
342 private static boolean isBasic(HttpServletRequest req) {
343 final String header = req.getHeader(HEAEDER_AUTHORIZATION);
344 return header == null ? false : header.startsWith("Basic");
347 private static boolean isBearer(HttpServletRequest req) {
348 final String header = req.getHeader(HEAEDER_AUTHORIZATION);
349 return header == null ? false : header.startsWith("Bearer");
352 private boolean rolesMatch(List<String> userRoles, List<String> policyRoles, boolean any) {
354 for (String policyRole : policyRoles) {
355 if (userRoles.contains(policyRole)) {
361 for (String policyRole : policyRoles) {
362 if (!userRoles.contains(policyRole)) {
371 public String getHost(HttpServletRequest req) {
372 String hostUrl = this.config.getPublicUrl();
373 if (hostUrl == null) {
374 final String tmp = req.getRequestURL().toString();
375 final String regex = "^(http[s]{0,1}:\\/\\/[^\\/]+)";
376 final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
377 final Matcher matcher = pattern.matcher(tmp);
378 if (matcher.find()) {
379 hostUrl = matcher.group(1);
382 LOG.info("host={}", hostUrl);
387 private List<PublicOAuthProviderConfig> getConfigs(Collection<AuthService> values) {
388 List<PublicOAuthProviderConfig> configs = new ArrayList<>();
389 for (AuthService svc : values) {
390 configs.add(svc.getConfig());
396 * GET /oauth/redirect/{providerID}
400 * @throws IOException
402 private void handleRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
403 final String uri = req.getRequestURI();
404 final Matcher matcher = REDIRECTID_PATTERN.matcher(uri);
405 if (matcher.find()) {
406 AuthService provider = this.providerStore.getOrDefault(matcher.group(1), null);
407 if (provider != null) {
408 //provider.setLocalHostUrl(getHost(req));
409 provider.handleRedirect(req, resp, getHost(req));
413 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
417 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
419 LOG.debug("POST request for {}", req.getRequestURI());
420 if (this.config.doSupportOdlUsers() && LOGINURI.equals(req.getRequestURI())) {
421 final String username = req.getParameter("username");
422 final String domain = req.getParameter("domain");
424 this.doLogin(username, req.getParameter("password"), domain != null ? domain : DEFAULT_DOMAIN);
426 sendResponse(resp, HttpServletResponse.SC_OK, new OAuthToken(token));
427 LOG.debug("login for odluser {} succeeded", username);
430 LOG.debug("login failed");
434 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
437 private BearerToken doLogin(String username, String password, String domain) {
438 if (!username.contains("@")) {
439 username = String.format("%s@%s", username, domain);
441 HttpServletRequest req = new HeadersOnlyHttpServletRequest(
442 Map.of("Authorization", BaseHTTPClient.getAuthorizationHeaderValue(username, password)));
443 if (this.odlAuthenticator.authenticate(req)) {
444 List<String> roles = this.odlIdentityService.listRoles(username, domain);
445 UserTokenPayload data = new UserTokenPayload();
446 data.setPreferredUsername(username);
447 data.setFamilyName("");
448 data.setGivenName(username);
449 data.setExp(this.tokenCreator.getDefaultExp());
450 data.setRoles(roles);
451 return this.tokenCreator.createNewJWT(data);
459 private void sendResponse(HttpServletResponse resp, int code, Object data) throws IOException {
460 byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
462 resp.setStatus(code);
463 resp.setContentLength(output.length);
464 resp.setContentType("application/json");
465 ServletOutputStream os = null;
466 os = resp.getOutputStream();
470 private void logout() {
471 final Subject subject = SecurityUtils.getSubject();
474 Session session = subject.getSession(false);
475 if (session != null) {
478 } catch (ShiroException e) {
479 LOG.debug("Couldn't log out {}", subject, e);