2 * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
9 package org.opendaylight.aaa.shiro.realm;
11 import com.google.common.base.Strings;
13 import java.util.ArrayList;
14 import java.util.HashMap;
15 import java.util.List;
19 import org.apache.shiro.authc.AuthenticationException;
20 import org.apache.shiro.authc.AuthenticationInfo;
21 import org.apache.shiro.authc.AuthenticationToken;
22 import org.apache.shiro.authc.SimpleAuthenticationInfo;
23 import org.apache.shiro.authc.UsernamePasswordToken;
24 import org.apache.shiro.authz.AuthorizationInfo;
25 import org.apache.shiro.authz.SimpleAuthorizationInfo;
26 import org.apache.shiro.codec.Base64;
27 import org.apache.shiro.realm.AuthorizingRealm;
28 import org.apache.shiro.subject.PrincipalCollection;
29 import org.opendaylight.aaa.api.Authentication;
30 import org.opendaylight.aaa.api.TokenAuth;
31 import org.opendaylight.aaa.basic.HttpBasicAuth;
32 import org.opendaylight.aaa.sts.ServiceLocator;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * TokenAuthRealm is an adapter between the AAA shiro subsystem and the existing
38 * <code>TokenAuth</code> mechanisms. Thus, one can enable use of
39 * <code>IDMStore</code> and <code>IDMMDSALStore</code>.
41 * @author Ryan Goulding (ryandgoulding@gmail.com)
43 public class TokenAuthRealm extends AuthorizingRealm {
45 private static final String USERNAME_DOMAIN_SEPARATOR = "@";
48 * The unique identifying name for <code>TokenAuthRealm</code>
50 private static final String TOKEN_AUTH_REALM_DEFAULT_NAME = "TokenAuthRealm";
53 * The message that is displayed if no <code>TokenAuth</code> interface is
56 private static final String AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE = "{\"error\":\"Authentication service unavailable\"}";
59 * The message that is displayed if credentials are missing or malformed
61 private static final String FATAL_ERROR_DECODING_CREDENTIALS = "{\"error\":\"Unable to decode credentials\"}";
64 * The message that is displayed if non-Basic Auth is attempted
66 private static final String FATAL_ERROR_BASIC_AUTH_ONLY = "{\"error\":\"Only basic authentication is supported by TokenAuthRealm\"}";
69 * The purposefully generic message displayed if <code>TokenAuth</code> is
70 * unable to validate the given credentials
72 private static final String UNABLE_TO_AUTHENTICATE = "{\"error\":\"Could not authenticate\"}";
74 private static final Logger LOG = LoggerFactory.getLogger(TokenAuthRealm.class);
76 public TokenAuthRealm() {
78 super.setName(TOKEN_AUTH_REALM_DEFAULT_NAME);
84 * Roles are derived from <code>TokenAuth.authenticate()</code>. Shiro roles
85 * are identical to existing IDM roles.
88 * org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache
89 * .shiro.subject.PrincipalCollection)
92 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
93 final Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
94 final ODLPrincipal odlPrincipal;
96 odlPrincipal = (ODLPrincipal) primaryPrincipal;
97 return new SimpleAuthorizationInfo(odlPrincipal.getRoles());
98 } catch(ClassCastException e) {
99 LOG.error("Couldn't decode authorization request", e);
101 return new SimpleAuthorizationInfo();
105 * Bridge new to old style <code>TokenAuth</code> interface.
107 * @param username The request username
108 * @param password The request password
109 * @param domain The request domain
110 * @return <code>username:password:domain</code>
112 static String getUsernamePasswordDomainString(final String username, final String password,
113 final String domain) {
114 return username + HttpBasicAuth.AUTH_SEP + password + HttpBasicAuth.AUTH_SEP + domain;
119 * @param credentialToken
120 * @return Base64 encoded token
122 static String getEncodedToken(final String credentialToken) {
123 return Base64.encodeToString(credentialToken.getBytes());
128 * @param encodedToken
129 * @return Basic <code>encodedToken</code>
131 static String getTokenAuthHeader(final String encodedToken) {
132 return HttpBasicAuth.BASIC_PREFIX + encodedToken;
137 * @param tokenAuthHeader
138 * @return a map with the basic auth header
140 Map<String, List<String>> formHeadersWithToken(final String tokenAuthHeader) {
141 final Map<String, List<String>> headers = new HashMap<String, List<String>>();
142 final List<String> headerValue = new ArrayList<String>();
143 headerValue.add(tokenAuthHeader);
144 headers.put(HttpBasicAuth.AUTH_HEADER, headerValue);
149 * Adapter between basic authentication mechanism and existing
150 * <code>TokenAuth</code> interface.
152 * @param username Username from the request
153 * @param password Password from the request
154 * @param domain Domain from the request
155 * @return input map for <code>TokenAuth.validate()</code>
157 Map<String, List<String>> formHeaders(final String username, final String password,
158 final String domain) {
159 String usernamePasswordToken = getUsernamePasswordDomainString(username, password, domain);
160 String encodedToken = getEncodedToken(usernamePasswordToken);
161 String tokenAuthHeader = getTokenAuthHeader(encodedToken);
162 return formHeadersWithToken(tokenAuthHeader);
166 * Adapter to check for available <code>TokenAuth<code> implementations.
170 boolean isTokenAuthAvailable() {
171 return ServiceLocator.getInstance().getAuthenticationService() != null;
177 * Authenticates against any <code>TokenAuth</code> registered with the
178 * <code>ServiceLocator</code>
181 * org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org
182 * .apache.shiro.authc.AuthenticationToken)
185 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
186 throws AuthenticationException {
188 String username = "";
189 String password = "";
190 String domain = HttpBasicAuth.DEFAULT_DOMAIN;
193 final String qualifiedUser = extractUsername(authenticationToken);
194 if (qualifiedUser.contains(USERNAME_DOMAIN_SEPARATOR)) {
195 final String [] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR);
197 username = qualifiedUserArray[0];
198 domain = qualifiedUserArray[1];
199 } catch (ArrayIndexOutOfBoundsException e) {
200 LOG.trace("Couldn't parse domain from {}; trying without one",
204 username = qualifiedUser;
206 password = extractPassword(authenticationToken);
208 } catch (NullPointerException e) {
209 throw new AuthenticationException(FATAL_ERROR_DECODING_CREDENTIALS, e);
210 } catch (ClassCastException e) {
211 throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY, e);
214 // check to see if there are TokenAuth implementations available
215 if (!isTokenAuthAvailable()) {
216 throw new AuthenticationException(AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE);
219 // if the password is empty, this is an OAuth2 request, not a Basic HTTP
221 if (!Strings.isNullOrEmpty(password)) {
222 if (ServiceLocator.getInstance().getAuthenticationService().isAuthEnabled()) {
223 Map<String, List<String>> headers = formHeaders(username, password, domain);
224 // iterate over <code>TokenAuth</code> implementations and
226 // authentication with each one
227 final List<TokenAuth> tokenAuthCollection = ServiceLocator.getInstance()
228 .getTokenAuthCollection();
229 for (TokenAuth ta : tokenAuthCollection) {
231 LOG.debug("Authentication attempt using {}", ta.getClass().getName());
232 final Authentication auth = ta.validate(headers);
234 LOG.debug("Authentication attempt successful");
235 ServiceLocator.getInstance().getAuthenticationService().set(auth);
236 final ODLPrincipal odlPrincipal = ODLPrincipal.createODLPrincipal(auth);
237 return new SimpleAuthenticationInfo(odlPrincipal, password.toCharArray(),
240 } catch (AuthenticationException ae) {
241 LOG.debug("Authentication attempt unsuccessful");
242 throw new AuthenticationException(UNABLE_TO_AUTHENTICATE, ae);
248 // extract the authentication token and attempt validation of the token
249 final String token = extractUsername(authenticationToken);
250 final Authentication auth;
252 auth = validate(token);
254 final ODLPrincipal odlPrincipal = ODLPrincipal.createODLPrincipal(auth);
255 return new SimpleAuthenticationInfo(odlPrincipal, "", getName());
257 } catch (AuthenticationException e) {
258 LOG.debug("Unknown OAuth2 Token Access Request", e);
261 LOG.debug("Authentication failed: exhausted TokenAuth resources");
265 private Authentication validate(final String token) {
266 Authentication auth = ServiceLocator.getInstance().getTokenStore().get(token);
268 throw new AuthenticationException("Could not validate the token " + token);
270 ServiceLocator.getInstance().getAuthenticationService().set(auth);
276 * extract the username from an <code>AuthenticationToken</code>
278 * @param authenticationToken
280 * @throws ClassCastException
281 * @throws NullPointerException
283 static String extractUsername(final AuthenticationToken authenticationToken)
284 throws ClassCastException, NullPointerException {
286 return (String) authenticationToken.getPrincipal();
290 * extract the password from an <code>AuthenticationToken</code>
292 * @param authenticationToken
294 * @throws ClassCastException
295 * @throws NullPointerException
297 static String extractPassword(final AuthenticationToken authenticationToken)
298 throws ClassCastException, NullPointerException {
300 final UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken;
301 return new String(upt.getPassword());
305 * Since <code>TokenAuthRealm</code> is an <code>AuthorizingRealm</code>, it supports
306 * individual steps for authentication and authorization. In ODL's existing <code>TokenAuth</code>
307 * mechanism, authentication and authorization are currently done in a single monolithic step.
308 * <code>ODLPrincipal</code> is abstracted as a DTO between the two steps. It fulfills the
309 * responsibility of a <code>Principal</code>, since it contains identification information
310 * but no credential information.
312 * @author Ryan Goulding (ryandgoulding@gmail.com)
314 private static class ODLPrincipal {
316 private final String username;
317 private final String domain;
318 private final String userId;
319 private final Set<String> roles;
321 private ODLPrincipal(final String username, final String domain, final String userId, final Set<String> roles) {
322 this.username = username;
323 this.domain = domain;
324 this.userId = userId;
329 * A static factory method to create <code>ODLPrincipal</code> instances.
331 * @param username The authenticated user
332 * @param domain The domain <code>username</code> belongs to.
333 * @param userId The unique key for <code>username</code>
334 * @param roles The roles associated with <code>username</code>@<code>domain</code>
335 * @return A Principal for the given session; essentially a DTO.
337 static ODLPrincipal createODLPrincipal(final String username, final String domain,
338 final String userId, final Set<String> roles) {
340 return new ODLPrincipal(username, domain, userId, roles);
344 * A static factory method to create <code>ODLPrincipal</code> instances.
346 * @param auth Contains identifying information for the particular request.
347 * @return A Principal for the given session; essentially a DTO.
349 static ODLPrincipal createODLPrincipal(final Authentication auth) {
350 return createODLPrincipal(auth.user(), auth.domain(), auth.userId(), auth.roles());
353 String getUsername() {
354 return this.username;
365 Set<String> getRoles() {