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;
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.HashMap;
31 import java.util.List;
33 import java.util.Optional;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 import javax.servlet.ServletException;
37 import javax.servlet.ServletOutputStream;
38 import javax.servlet.http.HttpServlet;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpServletResponse;
41 import org.apache.shiro.SecurityUtils;
42 import org.apache.shiro.ShiroException;
43 import org.apache.shiro.authc.BearerToken;
44 import org.apache.shiro.codec.Base64;
45 import org.apache.shiro.session.Session;
46 import org.apache.shiro.subject.Subject;
47 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.*;
48 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OdlShiroConfiguration.MainItem;
49 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.data.OdlShiroConfiguration.UrlItem;
50 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.filters.CustomizedMDSALDynamicAuthorizationFilter;
51 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService;
52 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.AuthService.PublicOAuthProviderConfig;
53 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.MdSalAuthorizationStore;
54 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.OAuthProviderFactory;
55 import org.onap.ccsdk.features.sdnr.wt.oauthprovider.providers.TokenCreator;
56 import org.opendaylight.aaa.api.AuthenticationException;
57 import org.opendaylight.aaa.api.Claim;
58 import org.opendaylight.aaa.api.PasswordCredentialAuth;
59 import org.opendaylight.aaa.api.PasswordCredentials;
60 import org.opendaylight.aaa.tokenauthrealm.auth.PasswordCredentialBuilder;
61 import org.opendaylight.mdsal.binding.api.DataBroker;
62 import org.osgi.service.http.HttpService;
63 import org.osgi.service.http.NamespaceException;
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 REDIRECTID_REGEX = "^\\" + BASEURI + "\\/redirect\\/([^\\/]+)$";
79 private static final String LOGIN_REDIRECT_REGEX = "^\\" + LOGINURI + "\\/([^\\/]+)$";
80 private static final Pattern REDIRECTID_PATTERN = Pattern.compile(REDIRECTID_REGEX);
81 private static final Pattern LOGIN_REDIRECT_PATTERN = Pattern.compile(LOGIN_REDIRECT_REGEX);
83 private static final String DEFAULT_DOMAIN = "sdn";
84 private static final String HEAEDER_AUTHORIZATION = "Authorization";
86 private static final String LOGOUT_REDIRECT_URL_PARAMETER = "redirect_uri";
87 private static final String CLASSNAME_ODLBASICAUTH =
88 "org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter";
89 private static final String CLASSNAME_ODLBEARERANDBASICAUTH =
90 "org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter2";
91 private static final String CLASSNAME_ODLMDSALAUTH =
92 "org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter";
93 public static final String LOGIN_REDIRECT_FORMAT = LOGINURI + "/%s";
94 private static final String URI_PRE = BASEURI;
96 private static final String CONFIGFILE ="/opt/opendaylight/etc/opendaylight/datastore/initial/config/aaa-app-config.xml";
97 private final ObjectMapper mapper;
98 /* state <=> AuthProviderService> */
99 private final Map<String, AuthService> providerStore;
100 private final TokenCreator tokenCreator;
101 private final Config config;
102 private static MdSalAuthorizationStore mdsalAuthStore;
103 private PasswordCredentialAuth passwordCredentialAuth;
104 private OdlShiroConfiguration shiroConfiguration;
106 public AuthHttpServlet() throws IllegalArgumentException, IOException, InvalidConfigurationException,
107 UnableToConfigureOAuthService {
110 public AuthHttpServlet(String shiroconfigfile) throws IllegalArgumentException, IOException, InvalidConfigurationException,
111 UnableToConfigureOAuthService {
112 this.config = Config.getInstance();
113 this.shiroConfiguration = loadShiroConfig(shiroconfigfile);
114 this.tokenCreator = TokenCreator.getInstance(this.config);
115 this.mapper = new ObjectMapper();
116 this.providerStore = new HashMap<>();
117 for (OAuthProviderConfig pc : config.getProviders()) {
118 this.providerStore.put(pc.getId(), OAuthProviderFactory.create(pc.getType(), pc,
119 this.config.getRedirectUri(), TokenCreator.getInstance(this.config)));
123 public void setDataBroker(DataBroker dataBroker) {
124 CustomizedMDSALDynamicAuthorizationFilter.setDataBroker(dataBroker);
125 mdsalAuthStore = new MdSalAuthorizationStore(dataBroker);
128 public void setPasswordCredentialAuth(PasswordCredentialAuth passwordCredentialAuth) {
129 this.passwordCredentialAuth = passwordCredentialAuth;
133 public void onUnbindService(HttpService httpService) {
134 httpService.unregister(AuthHttpServlet.URI_PRE);
138 public void onBindService(HttpService httpService)
139 throws ServletException, NamespaceException {
140 if (httpService == null) {
141 LOG.warn("Unable to inject HttpService into loader.");
143 httpService.registerServlet(AuthHttpServlet.URI_PRE, this, null, null);
144 LOG.info("oauth servlet registered.");
147 private static OdlShiroConfiguration loadShiroConfig(String filename) throws IOException {
148 OdlXmlMapper mapper = new OdlXmlMapper();
149 return mapper.readValue(new File(filename), OdlShiroConfiguration.class);
153 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
154 LOG.debug("GET request for {}", req.getRequestURI());
156 if (PROVIDERSURI.equals(req.getRequestURI())) {
157 this.sendResponse(resp, HttpServletResponse.SC_OK, getConfigs(this.providerStore.values()));
158 } else if (req.getRequestURI().startsWith(LOGINURI)) {
159 this.handleLoginRedirect(req, resp);
160 } else if (req.getRequestURI().equals(LOGOUTURI)) {
161 this.handleLogout(req, resp);
162 } else if (POLICIESURI.equals(req.getRequestURI())) {
163 this.sendResponse(resp, HttpServletResponse.SC_OK, this.getPoliciesForUser(req));
164 } else if (req.getRequestURI().startsWith(REDIRECTURI)) {
165 this.handleRedirect(req, resp);
167 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
172 private void handleLogout(HttpServletRequest req, HttpServletResponse resp) throws IOException {
173 final String bearerToken = this.tokenCreator.getBearerToken(req, true);
174 String redirectUrl = req.getParameter(LOGOUT_REDIRECT_URL_PARAMETER);
175 if (redirectUrl == null) {
176 redirectUrl = this.config.getPublicUrl();
178 UserTokenPayload userInfo = this.tokenCreator.decode(bearerToken);
179 if (bearerToken != null && userInfo != null && !userInfo.isInternal()) {
180 AuthService provider = this.providerStore.getOrDefault(userInfo.getProviderId(), null);
182 if (provider != null) {
183 provider.sendLogoutRedirectResponse(bearerToken, resp, redirectUrl);
189 resp.sendRedirect(redirectUrl);
193 private void handleLoginRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
194 final String uri = req.getRequestURI();
195 final Matcher matcher = LOGIN_REDIRECT_PATTERN.matcher(uri);
196 if (matcher.find()) {
197 final String id = matcher.group(1);
198 AuthService provider = this.providerStore.getOrDefault(id, null);
199 if (provider != null) {
200 String redirectUrl = getHost(req) + String.format(REDIRECTURI_FORMAT, id);
201 provider.sendLoginRedirectResponse(resp, redirectUrl);
205 this.sendResponse(resp, HttpServletResponse.SC_NOT_FOUND, "");
209 * find out what urls can be accessed by user and which are forbidden
211 * urlEntries: "anon" -> any access allowed "authcXXX" -> no grouping rule -> any access for user allowed "authcXXX,
212 * roles[abc] -> user needs to have role abc "authcXXX, roles["abc,def"] -> user needs to have roles abc AND def
213 * "authcXXX, anyroles[abc] -> user needs to have role abc "authcXXX, anyroles["abc,def"] -> user needs to have
219 private List<OdlPolicy> getPoliciesForUser(HttpServletRequest req) {
220 List<OdlPolicy> policies = new ArrayList<>();
221 List<UrlItem> urlRules = this.shiroConfiguration.getUrls();
222 UserTokenPayload data = this.getUserInfo(req);
223 if (urlRules != null) {
224 LOG.debug("try to find rules for user {} with roles {}",
225 data == null ? "null" : data.getPreferredUsername(), data == null ? "null" : data.getRoles());
226 final String regex = "^([^,]+)[,]?[\\ ]?([anyroles]+)?(\\[\"?([a-zA-Z,]+)\"?\\])?";
227 final Pattern pattern = Pattern.compile(regex);
229 for (UrlItem urlRule : urlRules) {
230 matcher = pattern.matcher(urlRule.getPairValue());
231 if (matcher.find()) {
233 final String authClass = getAuthClass(matcher.group(1));
234 Optional<OdlPolicy> policy = Optional.empty();
235 //anon access allowed
236 if (authClass == null) {
237 policy = Optional.of(OdlPolicy.allowAll(urlRule.getPairKey()));
238 } else if (authClass.equals(CLASSNAME_ODLBASICAUTH) || "authcBasic".equals(urlRule.getPairKey())) {
239 policy = isBasic(req) ? this.getTokenBasedPolicy(urlRule, matcher, data)
240 : Optional.of(OdlPolicy.denyAll(urlRule.getPairKey()));
241 } else if (authClass.equals(CLASSNAME_ODLBEARERANDBASICAUTH)) {
242 policy = this.getTokenBasedPolicy(urlRule, matcher, data);
243 } else if (authClass.equals(CLASSNAME_ODLMDSALAUTH)) {
244 policy = this.getMdSalBasedPolicy(urlRule, data);
246 if (policy.isPresent()) {
247 policies.add(policy.get());
249 LOG.warn("unable to get policy for authClass {} for entry {}", authClass,
250 urlRule.getPairValue());
251 policies.add(OdlPolicy.denyAll(urlRule.getPairKey()));
253 } catch (NoDefinitionFoundException e) {
254 LOG.warn("unknown authClass: ", e);
258 LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
262 LOG.debug("no url rules found");
268 * extract policy rule for user from MD-SAL not yet supported
274 private Optional<OdlPolicy> getMdSalBasedPolicy(UrlItem urlRule, UserTokenPayload data) {
275 if (mdsalAuthStore != null) {
276 return data != null ? mdsalAuthStore.getPolicy(urlRule.getPairKey(), data.getRoles())
277 : Optional.of(OdlPolicy.denyAll(urlRule.getPairKey()));
279 return Optional.empty();
283 * extract policy rule for user from url rules of config
290 private Optional<OdlPolicy> getTokenBasedPolicy(UrlItem urlRule, Matcher matcher,
291 UserTokenPayload data) {
292 final String url = urlRule.getPairKey();
293 final String rule = urlRule.getPairValue();
294 if (!rule.contains(",")) {
295 LOG.debug("found rule without roles for '{}'", matcher.group(1));
296 //not important if anon or authcXXX
297 if (data != null || "anon".equals(matcher.group(1))) {
298 return Optional.of(OdlPolicy.allowAll(url));
302 LOG.debug("found rule with roles '{}'", matcher.group(4));
303 if ("roles".equals(matcher.group(2))) {
304 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), false)) {
305 return Optional.of(OdlPolicy.allowAll(url));
307 return Optional.of(OdlPolicy.denyAll(url));
309 } else if ("anyroles".equals(matcher.group(2))) {
310 if (this.rolesMatch(data.getRoles(), Arrays.asList(matcher.group(4).split(",")), true)) {
311 return Optional.of(OdlPolicy.allowAll(url));
313 return Optional.of(OdlPolicy.denyAll(url));
316 LOG.warn("unable to detect url role value: {}", urlRule.getPairValue());
319 return Optional.of(OdlPolicy.denyAll(url));
321 return Optional.empty();
324 private String getAuthClass(String key) throws NoDefinitionFoundException {
325 if ("anon".equals(key)) {
328 if("authcBasic".equals(key)){
329 return CLASSNAME_ODLBASICAUTH;
331 List<MainItem> list = shiroConfiguration.getMain();
332 Optional<MainItem> main =
333 list == null ? Optional.empty() : list.stream().filter(e -> e.getPairKey().equals(key)).findFirst();
334 if (main.isPresent()) {
335 return main.get().getPairValue();
337 throw new NoDefinitionFoundException("unable to find def for " + key);
340 private UserTokenPayload getUserInfo(HttpServletRequest req) {
342 UserTokenPayload data = this.tokenCreator.decode(req);
346 } else if (isBasic(req)) {
347 String username = getBasicAuthUsername(req);
348 if (username != null) {
349 final String domain = getBasicAuthDomain(username);
350 if (!username.contains("@")) {
351 username = String.format("%s@%s", username, domain);
353 List<String> roles = List.of();// odlIdentityService.listRoles(username, domain);
354 return UserTokenPayload.createInternal(username, roles);
360 private static String getBasicAuthDomain(String username) {
361 if (username.contains("@")) {
362 return username.split("@")[1];
364 return DEFAULT_DOMAIN;
367 private static String getBasicAuthUsername(HttpServletRequest req) {
368 final String header = req.getHeader(HEAEDER_AUTHORIZATION);
369 final String decoded = Base64.decodeToString(header.substring(6));
370 // attempt to decode username/password; otherwise decode as token
371 if (decoded.contains(":")) {
372 return decoded.split(":")[0];
374 LOG.warn("unable to detect username from basicauth header {}", header);
378 private static boolean isBasic(HttpServletRequest req) {
379 final String header = req.getHeader(HEAEDER_AUTHORIZATION);
380 return header != null && header.startsWith("Basic");
383 private static boolean isBearer(HttpServletRequest req) {
384 final String header = req.getHeader(HEAEDER_AUTHORIZATION);
385 return header != null && header.startsWith("Bearer");
388 private boolean rolesMatch(List<String> userRoles, List<String> policyRoles, boolean any) {
390 for (String policyRole : policyRoles) {
391 if (userRoles.contains(policyRole)) {
397 for (String policyRole : policyRoles) {
398 if (!userRoles.contains(policyRole)) {
407 public String getHost(HttpServletRequest req) {
408 String hostUrl = this.config.getPublicUrl();
409 if (hostUrl == null) {
410 final String tmp = req.getRequestURL().toString();
411 final String regex = "^(http[s]{0,1}:\\/\\/[^\\/]+)";
412 final Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
413 final Matcher matcher = pattern.matcher(tmp);
414 if (matcher.find()) {
415 hostUrl = matcher.group(1);
418 LOG.info("host={}", hostUrl);
423 private List<PublicOAuthProviderConfig> getConfigs(Collection<AuthService> values) {
424 List<PublicOAuthProviderConfig> configs = new ArrayList<>();
425 for (AuthService svc : values) {
426 configs.add(svc.getConfig());
432 * GET /oauth/redirect/{providerID}
436 * @throws IOException
438 private void handleRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException {
439 final String uri = req.getRequestURI();
440 final Matcher matcher = REDIRECTID_PATTERN.matcher(uri);
441 if (matcher.find()) {
442 AuthService provider = this.providerStore.getOrDefault(matcher.group(1), null);
443 if (provider != null) {
444 //provider.setLocalHostUrl(getHost(req));
445 provider.handleRedirect(req, resp, getHost(req));
449 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
453 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
455 LOG.debug("POST request for {}", req.getRequestURI());
456 if (this.config.loginActive() && this.config.doSupportOdlUsers() && LOGINURI.equals(req.getRequestURI())) {
457 final String username = req.getParameter("username");
458 final String domain = req.getParameter("domain");
460 this.doLogin(username, req.getParameter("password"), domain != null ? domain : DEFAULT_DOMAIN);
462 sendResponse(resp, HttpServletResponse.SC_OK, new OAuthToken(token));
463 LOG.debug("login for odluser {} succeeded", username);
466 LOG.debug("login failed");
470 resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
473 private BearerToken doLogin(String username, String password, String domain) {
475 PasswordCredentials pc =
476 (new PasswordCredentialBuilder()).setUserName(username).setPassword(password).setDomain(domain).build();
479 claim = this.passwordCredentialAuth.authenticate(pc);
480 } catch (AuthenticationException e) {
481 LOG.warn("unable to authentication user {} for domain {}: ", username, domain, e);
484 List<String> roles = claim.roles().stream().toList();//odlIdentityService.listRoles(username, domain);
485 UserTokenPayload data = new UserTokenPayload();
486 data.setPreferredUsername(username);
487 data.setFamilyName("");
488 data.setGivenName(username);
489 data.setIat(this.tokenCreator.getDefaultIat());
490 data.setExp(this.tokenCreator.getDefaultExp());
491 data.setRoles(roles);
492 return this.tokenCreator.createNewJWT(data);
494 LOG.info("unable to read auth from authservice");
500 /* private void sendResponse(HttpServletResponse resp, int code) throws IOException {
501 this.sendResponse(resp, code, null);
504 private void sendResponse(HttpServletResponse resp, int code, Object data) throws IOException {
505 byte[] output = data != null ? mapper.writeValueAsString(data).getBytes() : new byte[0];
507 resp.setStatus(code);
508 resp.setContentLength(output.length);
509 resp.setContentType("application/json");
510 ServletOutputStream os = resp.getOutputStream();
515 private void logout() {
516 /* final Subject subject = SecurityUtils.getSubject();
519 Session session = subject.getSession(false);
520 if (session != null) {
523 } catch (ShiroException e) {
524 LOG.debug("Couldn't log out {}", subject, e);