From cf864337c13b4638c588badf3f589f9e39318c95 Mon Sep 17 00:00:00 2001 From: Trevor Bramwell Date: Mon, 12 Sep 2016 11:06:56 -0700 Subject: Move ODL-AAA-MOON under 'upstream' Directory Change-Id: Ie010fbe3899e151421940908dbe8675aade54e2d Signed-off-by: Trevor Bramwell --- .../aaa/shiro/realm/TokenAuthRealm.java | 369 +++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java (limited to 'upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java') diff --git a/upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java b/upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java new file mode 100644 index 00000000..f9ae5051 --- /dev/null +++ b/upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java @@ -0,0 +1,369 @@ +/* + * 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.realm; + +import com.google.common.base.Strings; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.codec.Base64; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.basic.HttpBasicAuth; +import org.opendaylight.aaa.sts.ServiceLocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TokenAuthRealm is an adapter between the AAA shiro subsystem and the existing + * TokenAuth mechanisms. Thus, one can enable use of + * IDMStore and IDMMDSALStore. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class TokenAuthRealm extends AuthorizingRealm { + + private static final String USERNAME_DOMAIN_SEPARATOR = "@"; + + /** + * The unique identifying name for TokenAuthRealm + */ + private static final String TOKEN_AUTH_REALM_DEFAULT_NAME = "TokenAuthRealm"; + + /** + * The message that is displayed if no TokenAuth interface is + * available yet + */ + private static final String AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE = "{\"error\":\"Authentication service unavailable\"}"; + + /** + * The message that is displayed if credentials are missing or malformed + */ + private static final String FATAL_ERROR_DECODING_CREDENTIALS = "{\"error\":\"Unable to decode credentials\"}"; + + /** + * The message that is displayed if non-Basic Auth is attempted + */ + private static final String FATAL_ERROR_BASIC_AUTH_ONLY = "{\"error\":\"Only basic authentication is supported by TokenAuthRealm\"}"; + + /** + * The purposefully generic message displayed if TokenAuth is + * unable to validate the given credentials + */ + private static final String UNABLE_TO_AUTHENTICATE = "{\"error\":\"Could not authenticate\"}"; + + private static final Logger LOG = LoggerFactory.getLogger(TokenAuthRealm.class); + + public TokenAuthRealm() { + super(); + super.setName(TOKEN_AUTH_REALM_DEFAULT_NAME); + } + + /* + * (non-Javadoc) + * + * Roles are derived from TokenAuth.authenticate(). Shiro roles + * are identical to existing IDM roles. + * + * @see + * org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache + * .shiro.subject.PrincipalCollection) + */ + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { + final Object primaryPrincipal = principalCollection.getPrimaryPrincipal(); + final ODLPrincipal odlPrincipal; + try { + odlPrincipal = (ODLPrincipal) primaryPrincipal; + return new SimpleAuthorizationInfo(odlPrincipal.getRoles()); + } catch(ClassCastException e) { + LOG.error("Couldn't decode authorization request", e); + } + return new SimpleAuthorizationInfo(); + } + + /** + * Bridge new to old style TokenAuth interface. + * + * @param username The request username + * @param password The request password + * @param domain The request domain + * @return username:password:domain + */ + static String getUsernamePasswordDomainString(final String username, final String password, + final String domain) { + return username + HttpBasicAuth.AUTH_SEP + password + HttpBasicAuth.AUTH_SEP + domain; + } + + /** + * + * @param credentialToken + * @return Base64 encoded token + */ + static String getEncodedToken(final String credentialToken) { + return Base64.encodeToString(credentialToken.getBytes()); + } + + /** + * + * @param encodedToken + * @return Basic encodedToken + */ + static String getTokenAuthHeader(final String encodedToken) { + return HttpBasicAuth.BASIC_PREFIX + encodedToken; + } + + /** + * + * @param tokenAuthHeader + * @return a map with the basic auth header + */ + Map> formHeadersWithToken(final String tokenAuthHeader) { + final Map> headers = new HashMap>(); + final List headerValue = new ArrayList(); + headerValue.add(tokenAuthHeader); + headers.put(HttpBasicAuth.AUTH_HEADER, headerValue); + return headers; + } + + /** + * Adapter between basic authentication mechanism and existing + * TokenAuth interface. + * + * @param username Username from the request + * @param password Password from the request + * @param domain Domain from the request + * @return input map for TokenAuth.validate() + */ + Map> formHeaders(final String username, final String password, + final String domain) { + String usernamePasswordToken = getUsernamePasswordDomainString(username, password, domain); + String encodedToken = getEncodedToken(usernamePasswordToken); + String tokenAuthHeader = getTokenAuthHeader(encodedToken); + return formHeadersWithToken(tokenAuthHeader); + } + + /** + * Adapter to check for available TokenAuth implementations. + * + * @return + */ + boolean isTokenAuthAvailable() { + return ServiceLocator.getInstance().getAuthenticationService() != null; + } + + /* + * (non-Javadoc) + * + * Authenticates against any TokenAuth registered with the + * ServiceLocator + * + * @see + * org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org + * .apache.shiro.authc.AuthenticationToken) + */ + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) + throws AuthenticationException { + + String username = ""; + String password = ""; + String domain = HttpBasicAuth.DEFAULT_DOMAIN; + + try { + final String qualifiedUser = extractUsername(authenticationToken); + if (qualifiedUser.contains(USERNAME_DOMAIN_SEPARATOR)) { + final String [] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR); + try { + username = qualifiedUserArray[0]; + domain = qualifiedUserArray[1]; + } catch (ArrayIndexOutOfBoundsException e) { + LOG.trace("Couldn't parse domain from {}; trying without one", + qualifiedUser, e); + } + } else { + username = qualifiedUser; + } + password = extractPassword(authenticationToken); + + } catch (NullPointerException e) { + throw new AuthenticationException(FATAL_ERROR_DECODING_CREDENTIALS, e); + } catch (ClassCastException e) { + throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY, e); + } + + // check to see if there are TokenAuth implementations available + if (!isTokenAuthAvailable()) { + throw new AuthenticationException(AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE); + } + + // 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, domain); + // 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()); + final Authentication auth = ta.validate(headers); + if (auth != null) { + LOG.debug("Authentication attempt successful"); + ServiceLocator.getInstance().getAuthenticationService().set(auth); + final ODLPrincipal odlPrincipal = ODLPrincipal.createODLPrincipal(auth); + return new SimpleAuthenticationInfo(odlPrincipal, password.toCharArray(), + getName()); + } + } catch (AuthenticationException ae) { + LOG.debug("Authentication attempt unsuccessful"); + 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) { + final ODLPrincipal odlPrincipal = ODLPrincipal.createODLPrincipal(auth); + return new SimpleAuthenticationInfo(odlPrincipal, "", getName()); + } + } catch (AuthenticationException e) { + LOG.debug("Unknown OAuth2 Token Access Request", e); + } + + LOG.debug("Authentication failed: exhausted TokenAuth resources"); + 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 + * + * @param authenticationToken + * @return + * @throws ClassCastException + * @throws NullPointerException + */ + static String extractUsername(final AuthenticationToken authenticationToken) + throws ClassCastException, NullPointerException { + + return (String) authenticationToken.getPrincipal(); + } + + /** + * extract the password from an AuthenticationToken + * + * @param authenticationToken + * @return + * @throws ClassCastException + * @throws NullPointerException + */ + static String extractPassword(final AuthenticationToken authenticationToken) + throws ClassCastException, NullPointerException { + + final UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken; + return new String(upt.getPassword()); + } + + /** + * Since TokenAuthRealm is an AuthorizingRealm, it supports + * individual steps for authentication and authorization. In ODL's existing TokenAuth + * mechanism, authentication and authorization are currently done in a single monolithic step. + * ODLPrincipal is abstracted as a DTO between the two steps. It fulfills the + * responsibility of a Principal, since it contains identification information + * but no credential information. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ + private static class ODLPrincipal { + + private final String username; + private final String domain; + private final String userId; + private final Set roles; + + private ODLPrincipal(final String username, final String domain, final String userId, final Set roles) { + this.username = username; + this.domain = domain; + this.userId = userId; + this.roles = roles; + } + + /** + * A static factory method to create ODLPrincipal instances. + * + * @param username The authenticated user + * @param domain The domain username belongs to. + * @param userId The unique key for username + * @param roles The roles associated with username@domain + * @return A Principal for the given session; essentially a DTO. + */ + static ODLPrincipal createODLPrincipal(final String username, final String domain, + final String userId, final Set roles) { + + return new ODLPrincipal(username, domain, userId, roles); + } + + /** + * A static factory method to create ODLPrincipal instances. + * + * @param auth Contains identifying information for the particular request. + * @return A Principal for the given session; essentially a DTO. + */ + static ODLPrincipal createODLPrincipal(final Authentication auth) { + return createODLPrincipal(auth.user(), auth.domain(), auth.userId(), auth.roles()); + } + + String getUsername() { + return this.username; + } + + String getDomain() { + return this.domain; + } + + String getUserId() { + return this.userId; + } + + Set getRoles() { + return this.roles; + } + } +} -- cgit 1.2.3-korg