/* * 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; } } }