From: Ryan Goulding Date: Tue, 24 Nov 2015 03:09:30 +0000 (-0500) Subject: Add OAuth2 Token to Shiro to maintain cross-version compatibility X-Git-Tag: release/beryllium~66^2 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F10%2F30110%2F4;p=aaa.git Add OAuth2 Token to Shiro to maintain cross-version compatibility Adds a mechanism to authenticate OAuth2 tokens, similar to TokenAuthFilter's implementation. Change-Id: Idade3da6fc364e1635d8a92b37e617d5ca697821 Signed-off-by: Ryan Goulding --- diff --git a/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java b/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java new file mode 100644 index 000000000..1744dcfba --- /dev/null +++ b/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.shiro.filters; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.codec.Base64; +import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extends BasicHttpAuthenticationFilter to include ability to + * authenticate OAuth2 tokens, which is needed for backwards compatibility + * with TokenAuthFilter. + * + * This behavior is enabled by default for backwards compatibility. To disable + * OAuth2 functionality, just comment out the following line from the + * etc/shiro.ini file: + * authcBasic = org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter + * then restart the karaf container. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class ODLHttpAuthenticationFilter extends BasicHttpAuthenticationFilter { + + private static final Logger LOG = LoggerFactory.getLogger(ODLHttpAuthenticationFilter.class); + + // defined in lower-case for more efficient string comparison + protected static final String BEARER_SCHEME = "bearer"; + + public ODLHttpAuthenticationFilter() { + super(); + LOG.info("Creating the ODLHttpAuthenticationFilter"); + } + + @Override + protected String[] getPrincipalsAndCredentials(String scheme, String encoded) { + final String decoded = Base64.decodeToString(encoded); + // attempt to decode username/password; otherwise decode as token + if(decoded.contains(":")) { + return decoded.split(":"); + } + return new String[]{encoded}; + } + + @Override + protected boolean isLoginAttempt(String authzHeader) { + final String authzScheme = getAuthzScheme().toLowerCase(); + final String authzHeaderLowerCase = authzHeader.toLowerCase(); + return authzHeaderLowerCase.startsWith(authzScheme) || authzHeaderLowerCase.startsWith(BEARER_SCHEME); + } + +} diff --git a/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java b/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java index 283904ca2..08911d21b 100644 --- a/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java +++ b/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java @@ -30,6 +30,8 @@ import org.opendaylight.aaa.sts.ServiceLocator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Strings; + /** * TokenAuthRealm is an adapter between the AAA shiro subsystem and the existing * TokenAuth mechanisms. Thus, one can enable use of @@ -190,38 +192,66 @@ public class TokenAuthRealm extends AuthorizingRealm { throw new AuthenticationException(AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE); } - if (ServiceLocator.getInstance().getAuthenticationService().isAuthEnabled()) { - Map> headers = formHeaders(username,password); - - // iterate over TokenAuth implementations and attempt to - // authentication with each one - final List tokenAuthCollection = - ServiceLocator.getInstance().getTokenAuthCollection(); - for (TokenAuth ta : tokenAuthCollection) { - try { - LOG.debug("Authentication attempt using " + ta.getClass().getName()); - Authentication auth = ta.validate(headers); - if (auth != null) { - LOG.debug("Authentication attempt successful"); - ServiceLocator.getInstance().getAuthenticationService().set(auth); - this.cachedAuthenticationToken = auth; - return new SimpleAuthenticationInfo( - username, password.toCharArray(), getName()); + // if the password is empty, this is an OAuth2 request, not a Basic HTTP Auth request + if (!Strings.isNullOrEmpty(password)) { + if (ServiceLocator.getInstance().getAuthenticationService().isAuthEnabled()) { + Map> headers = formHeaders(username,password); + // iterate over TokenAuth implementations and attempt to + // authentication with each one + final List tokenAuthCollection = + ServiceLocator.getInstance().getTokenAuthCollection(); + for (TokenAuth ta : tokenAuthCollection) { + try { + LOG.debug("Authentication attempt using " + ta.getClass().getName()); + Authentication auth = ta.validate(headers); + if (auth != null) { + LOG.debug("Authentication attempt successful"); + ServiceLocator.getInstance().getAuthenticationService().set(auth); + this.cachedAuthenticationToken = auth; + return new SimpleAuthenticationInfo( + username, password.toCharArray(), getName()); + } + } catch (AuthenticationException ae) { + LOG.debug("Authentication attempt unsuccessful"); + // invalidate cached token + cachedAuthenticationToken = null; + throw new AuthenticationException(UNABLE_TO_AUTHENTICATE, ae); } - } catch (AuthenticationException ae) { - LOG.debug("Authentication attempt unsuccessful"); - // invalidate cached token - cachedAuthenticationToken = null; - throw new AuthenticationException(UNABLE_TO_AUTHENTICATE, ae); } } } + + // extract the authentication token and attempt validation of the token + final String token = extractUsername(authenticationToken); + final Authentication auth; + try { + auth = validate(token); + if (auth != null) { + cachedAuthenticationToken = auth; + return new SimpleAuthenticationInfo(auth.user(), + "", getName()); + } + } catch(AuthenticationException e) { + cachedAuthenticationToken = null; + LOG.info("Unknown OAuth2 Token Access Request", e); + } + LOG.debug("Authentication failed: exhausted TokenAuth resources"); // invalidate cached token cachedAuthenticationToken = null; return null; } + private Authentication validate(final String token) { + Authentication auth = ServiceLocator.getInstance().getTokenStore().get(token); + if (auth == null) { + throw new AuthenticationException("Could not validate the token " + token); + } else { + ServiceLocator.getInstance().getAuthenticationService().set(auth); + } + return auth; + } + /** * extract the username from an AuthenticationToken * diff --git a/aaa-shiro/src/main/resources/shiro.ini b/aaa-shiro/src/main/resources/shiro.ini index 632cfde97..75862161b 100644 --- a/aaa-shiro/src/main/resources/shiro.ini +++ b/aaa-shiro/src/main/resources/shiro.ini @@ -21,6 +21,10 @@ # bridge to existing authentication/authorization mechanisms tokenAuthRealm = org.opendaylight.aaa.shiro.realm.TokenAuthRealm securityManager.realms = $tokenAuthRealm +# adds a custom AuthenticationFilter to support OAuth2 for backwards +# compatibility. To disable, just comment out the next line +# and authcBasic will default to BasicHttpAuthenticationFilter +authcBasic = org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter [urls] # general access requires valid credentials (AuthN only)