Add OAuth2 Token to Shiro to maintain cross-version compatibility 10/30110/4
authorRyan Goulding <ryandgoulding@gmail.com>
Tue, 24 Nov 2015 03:09:30 +0000 (22:09 -0500)
committerRyan Goulding <ryandgoulding@gmail.com>
Wed, 25 Nov 2015 17:36:46 +0000 (12:36 -0500)
Adds a mechanism to authenticate OAuth2 tokens, similar to
TokenAuthFilter's implementation.

Change-Id: Idade3da6fc364e1635d8a92b37e617d5ca697821
Signed-off-by: Ryan Goulding <ryandgoulding@gmail.com>
aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java [new file with mode: 0644]
aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java
aaa-shiro/src/main/resources/shiro.ini

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 (file)
index 0000000..1744dcf
--- /dev/null
@@ -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 <code>BasicHttpAuthenticationFilter</code> to include ability to
+ * authenticate OAuth2 tokens, which is needed for backwards compatibility
+ * with <code>TokenAuthFilter</code>.
+ *
+ * This behavior is enabled by default for backwards compatibility.  To disable
+ * OAuth2 functionality, just comment out the following line from the
+ * <code>etc/shiro.ini</code> file:
+ * <code>authcBasic = org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter</code>
+ * 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);
+    }
+
+}
index 283904ca292179b725d2f3f26da25243beb4fda2..08911d21bdd783a5098e6f06dc07325353341ed5 100644 (file)
@@ -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
  * <code>TokenAuth</code> 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<String, List<String>> headers = formHeaders(username,password);
-
-            // iterate over <code>TokenAuth</code> implementations and attempt to
-            // authentication with each one
-            final List<TokenAuth> 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<String, List<String>> headers = formHeaders(username,password);
+                // iterate over <code>TokenAuth</code> implementations and attempt to
+                // authentication with each one
+                final List<TokenAuth> 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 <code>AuthenticationToken</code>
      *
index 632cfde978eedbabef8ca211c71e79c77bc556f6..75862161b0040ab804dd291b63f69206ac158b8a 100644 (file)
 # 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)