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