org.opendaylight.aaa.shiro.realm.TokenAuthRealm.java Source code

Java tutorial

Introduction

Here is the source code for org.opendaylight.aaa.shiro.realm.TokenAuthRealm.java

Source

/*
 * 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
 * <code>TokenAuth</code> mechanisms. Thus, one can enable use of
 * <code>IDMStore</code> and <code>IDMMDSALStore</code>.
 *
 * @author Ryan Goulding (ryandgoulding@gmail.com)
 */
public class TokenAuthRealm extends AuthorizingRealm {

    private static final String USERNAME_DOMAIN_SEPARATOR = "@";

    /**
     * The unique identifying name for <code>TokenAuthRealm</code>
     */
    private static final String TOKEN_AUTH_REALM_DEFAULT_NAME = "TokenAuthRealm";

    /**
     * The message that is displayed if no <code>TokenAuth</code> 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 <code>TokenAuth</code> 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 <code>TokenAuth.authenticate()</code>. 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 <code>TokenAuth</code> interface.
     *
     * @param username The request username
     * @param password The request password
     * @param domain The request domain
     * @return <code>username:password:domain</code>
     */
    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 <code>encodedToken</code>
     */
    static String getTokenAuthHeader(final String encodedToken) {
        return HttpBasicAuth.BASIC_PREFIX + encodedToken;
    }

    /**
     *
     * @param tokenAuthHeader
     * @return a map with the basic auth header
     */
    Map<String, List<String>> formHeadersWithToken(final String tokenAuthHeader) {
        final Map<String, List<String>> headers = new HashMap<String, List<String>>();
        final List<String> headerValue = new ArrayList<String>();
        headerValue.add(tokenAuthHeader);
        headers.put(HttpBasicAuth.AUTH_HEADER, headerValue);
        return headers;
    }

    /**
     * Adapter between basic authentication mechanism and existing
     * <code>TokenAuth</code> interface.
     *
     * @param username Username from the request
     * @param password Password from the request
     * @param domain Domain from the request
     * @return input map for <code>TokenAuth.validate()</code>
     */
    Map<String, List<String>> 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 <code>TokenAuth<code> implementations.
     *
     * @return
     */
    boolean isTokenAuthAvailable() {
        return ServiceLocator.getInstance().getAuthenticationService() != null;
    }

    /*
     * (non-Javadoc)
     *
     * Authenticates against any <code>TokenAuth</code> registered with the
     * <code>ServiceLocator</code>
     *
     * @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<String, List<String>> headers = formHeaders(username, password, domain);
                // 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());
                        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 <code>AuthenticationToken</code>
     *
     * @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 <code>AuthenticationToken</code>
     *
     * @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 <code>TokenAuthRealm</code> is an <code>AuthorizingRealm</code>, it supports
     * individual steps for authentication and authorization.  In ODL's existing <code>TokenAuth</code>
     * mechanism, authentication and authorization are currently done in a single monolithic step.
     * <code>ODLPrincipal</code> is abstracted as a DTO between the two steps.  It fulfills the
     * responsibility of a <code>Principal</code>, 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<String> roles;

        private ODLPrincipal(final String username, final String domain, final String userId,
                final Set<String> roles) {
            this.username = username;
            this.domain = domain;
            this.userId = userId;
            this.roles = roles;
        }

        /**
         * A static factory method to create <code>ODLPrincipal</code> instances.
         *
         * @param username The authenticated user
         * @param domain The domain <code>username</code> belongs to.
         * @param userId The unique key for <code>username</code>
         * @param roles The roles associated with <code>username</code>@<code>domain</code>
         * @return A Principal for the given session;  essentially a DTO.
         */
        static ODLPrincipal createODLPrincipal(final String username, final String domain, final String userId,
                final Set<String> roles) {

            return new ODLPrincipal(username, domain, userId, roles);
        }

        /**
         * A static factory method to create <code>ODLPrincipal</code> 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<String> getRoles() {
            return this.roles;
        }
    }
}