f9ae5051635dbbe757679c17ef21e40a2b83707f
[moon.git] /
1 /*
2  * Copyright (c) 2015 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8
9 package org.opendaylight.aaa.shiro.realm;
10
11 import com.google.common.base.Strings;
12
13 import java.util.ArrayList;
14 import java.util.HashMap;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Set;
18
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;
35
36 /**
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>.
40  *
41  * @author Ryan Goulding (ryandgoulding@gmail.com)
42  */
43 public class TokenAuthRealm extends AuthorizingRealm {
44
45     private static final String USERNAME_DOMAIN_SEPARATOR = "@";
46
47     /**
48      * The unique identifying name for <code>TokenAuthRealm</code>
49      */
50     private static final String TOKEN_AUTH_REALM_DEFAULT_NAME = "TokenAuthRealm";
51
52     /**
53      * The message that is displayed if no <code>TokenAuth</code> interface is
54      * available yet
55      */
56     private static final String AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE = "{\"error\":\"Authentication service unavailable\"}";
57
58     /**
59      * The message that is displayed if credentials are missing or malformed
60      */
61     private static final String FATAL_ERROR_DECODING_CREDENTIALS = "{\"error\":\"Unable to decode credentials\"}";
62
63     /**
64      * The message that is displayed if non-Basic Auth is attempted
65      */
66     private static final String FATAL_ERROR_BASIC_AUTH_ONLY = "{\"error\":\"Only basic authentication is supported by TokenAuthRealm\"}";
67
68     /**
69      * The purposefully generic message displayed if <code>TokenAuth</code> is
70      * unable to validate the given credentials
71      */
72     private static final String UNABLE_TO_AUTHENTICATE = "{\"error\":\"Could not authenticate\"}";
73
74     private static final Logger LOG = LoggerFactory.getLogger(TokenAuthRealm.class);
75
76     public TokenAuthRealm() {
77         super();
78         super.setName(TOKEN_AUTH_REALM_DEFAULT_NAME);
79     }
80
81     /*
82      * (non-Javadoc)
83      *
84      * Roles are derived from <code>TokenAuth.authenticate()</code>. Shiro roles
85      * are identical to existing IDM roles.
86      *
87      * @see
88      * org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache
89      * .shiro.subject.PrincipalCollection)
90      */
91     @Override
92     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
93         final Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
94         final ODLPrincipal odlPrincipal;
95         try {
96             odlPrincipal = (ODLPrincipal) primaryPrincipal;
97             return new SimpleAuthorizationInfo(odlPrincipal.getRoles());
98         } catch(ClassCastException e) {
99             LOG.error("Couldn't decode authorization request", e);
100         }
101         return new SimpleAuthorizationInfo();
102     }
103
104     /**
105      * Bridge new to old style <code>TokenAuth</code> interface.
106      *
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>
111      */
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;
115     }
116
117     /**
118      *
119      * @param credentialToken
120      * @return Base64 encoded token
121      */
122     static String getEncodedToken(final String credentialToken) {
123         return Base64.encodeToString(credentialToken.getBytes());
124     }
125
126     /**
127      *
128      * @param encodedToken
129      * @return Basic <code>encodedToken</code>
130      */
131     static String getTokenAuthHeader(final String encodedToken) {
132         return HttpBasicAuth.BASIC_PREFIX + encodedToken;
133     }
134
135     /**
136      *
137      * @param tokenAuthHeader
138      * @return a map with the basic auth header
139      */
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);
145         return headers;
146     }
147
148     /**
149      * Adapter between basic authentication mechanism and existing
150      * <code>TokenAuth</code> interface.
151      *
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>
156      */
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);
163     }
164
165     /**
166      * Adapter to check for available <code>TokenAuth<code> implementations.
167      *
168      * @return
169      */
170     boolean isTokenAuthAvailable() {
171         return ServiceLocator.getInstance().getAuthenticationService() != null;
172     }
173
174     /*
175      * (non-Javadoc)
176      *
177      * Authenticates against any <code>TokenAuth</code> registered with the
178      * <code>ServiceLocator</code>
179      *
180      * @see
181      * org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org
182      * .apache.shiro.authc.AuthenticationToken)
183      */
184     @Override
185     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
186             throws AuthenticationException {
187
188         String username = "";
189         String password = "";
190         String domain = HttpBasicAuth.DEFAULT_DOMAIN;
191
192         try {
193             final String qualifiedUser = extractUsername(authenticationToken);
194             if (qualifiedUser.contains(USERNAME_DOMAIN_SEPARATOR)) {
195                 final String [] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR);
196                 try {
197                     username = qualifiedUserArray[0];
198                     domain = qualifiedUserArray[1];
199                 } catch (ArrayIndexOutOfBoundsException e) {
200                     LOG.trace("Couldn't parse domain from {}; trying without one",
201                             qualifiedUser, e);
202                 }
203             } else {
204                 username = qualifiedUser;
205             }
206             password = extractPassword(authenticationToken);
207
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);
212         }
213
214         // check to see if there are TokenAuth implementations available
215         if (!isTokenAuthAvailable()) {
216             throw new AuthenticationException(AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE);
217         }
218
219         // if the password is empty, this is an OAuth2 request, not a Basic HTTP
220         // Auth request
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
225                 // attempt to
226                 // authentication with each one
227                 final List<TokenAuth> tokenAuthCollection = ServiceLocator.getInstance()
228                         .getTokenAuthCollection();
229                 for (TokenAuth ta : tokenAuthCollection) {
230                     try {
231                         LOG.debug("Authentication attempt using {}", ta.getClass().getName());
232                         final Authentication auth = ta.validate(headers);
233                         if (auth != null) {
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(),
238                                     getName());
239                         }
240                     } catch (AuthenticationException ae) {
241                         LOG.debug("Authentication attempt unsuccessful");
242                         throw new AuthenticationException(UNABLE_TO_AUTHENTICATE, ae);
243                     }
244                 }
245             }
246         }
247
248         // extract the authentication token and attempt validation of the token
249         final String token = extractUsername(authenticationToken);
250         final Authentication auth;
251         try {
252             auth = validate(token);
253             if (auth != null) {
254                 final ODLPrincipal odlPrincipal = ODLPrincipal.createODLPrincipal(auth);
255                 return new SimpleAuthenticationInfo(odlPrincipal, "", getName());
256             }
257         } catch (AuthenticationException e) {
258             LOG.debug("Unknown OAuth2 Token Access Request", e);
259         }
260
261         LOG.debug("Authentication failed: exhausted TokenAuth resources");
262         return null;
263     }
264
265     private Authentication validate(final String token) {
266         Authentication auth = ServiceLocator.getInstance().getTokenStore().get(token);
267         if (auth == null) {
268             throw new AuthenticationException("Could not validate the token " + token);
269         } else {
270             ServiceLocator.getInstance().getAuthenticationService().set(auth);
271         }
272         return auth;
273     }
274
275     /**
276      * extract the username from an <code>AuthenticationToken</code>
277      *
278      * @param authenticationToken
279      * @return
280      * @throws ClassCastException
281      * @throws NullPointerException
282      */
283     static String extractUsername(final AuthenticationToken authenticationToken)
284             throws ClassCastException, NullPointerException {
285
286         return (String) authenticationToken.getPrincipal();
287     }
288
289     /**
290      * extract the password from an <code>AuthenticationToken</code>
291      *
292      * @param authenticationToken
293      * @return
294      * @throws ClassCastException
295      * @throws NullPointerException
296      */
297     static String extractPassword(final AuthenticationToken authenticationToken)
298             throws ClassCastException, NullPointerException {
299
300         final UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken;
301         return new String(upt.getPassword());
302     }
303
304     /**
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.
311      *
312      * @author Ryan Goulding (ryandgoulding@gmail.com)
313      */
314     private static class ODLPrincipal {
315
316         private final String username;
317         private final String domain;
318         private final String userId;
319         private final Set<String> roles;
320
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;
325             this.roles = roles;
326         }
327
328         /**
329          * A static factory method to create <code>ODLPrincipal</code> instances.
330          *
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.
336          */
337         static ODLPrincipal createODLPrincipal(final String username, final String domain,
338                 final String userId, final Set<String> roles) {
339
340             return new ODLPrincipal(username, domain, userId, roles);
341         }
342
343         /**
344          * A static factory method to create <code>ODLPrincipal</code> instances.
345          *
346          * @param auth Contains identifying information for the particular request.
347          * @return A Principal for the given session;  essentially a DTO.
348          */
349         static ODLPrincipal createODLPrincipal(final Authentication auth) {
350             return createODLPrincipal(auth.user(), auth.domain(), auth.userId(), auth.roles());
351         }
352
353         String getUsername() {
354             return this.username;
355         }
356
357         String getDomain() {
358             return this.domain;
359         }
360
361         String getUserId() {
362             return this.userId;
363         }
364
365         Set<String> getRoles() {
366             return this.roles;
367         }
368     }
369 }