Fix AAF Locator for ONAP
[aaf/authz.git] / cadi / aaf / src / main / java / org / onap / aaf / cadi / oauth / TokenClient.java
1 /**
2  * ============LICENSE_START====================================================
3  * org.onap.aaf
4  * ===========================================================================
5  * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
6  * ===========================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END====================================================
19  *
20  */
21
22 package org.onap.aaf.cadi.oauth;
23
24 import java.io.IOException;
25 import java.net.ConnectException;
26 import java.net.URLEncoder;
27 import java.security.NoSuchAlgorithmException;
28 import java.util.ArrayList;
29 import java.util.List;
30
31 import org.onap.aaf.cadi.Access;
32 import org.onap.aaf.cadi.CadiException;
33 import org.onap.aaf.cadi.Hash;
34 import org.onap.aaf.cadi.LocatorException;
35 import org.onap.aaf.cadi.SecuritySetter;
36 import org.onap.aaf.cadi.Access.Level;
37 import org.onap.aaf.cadi.aaf.v2_0.AAFCon;
38 import org.onap.aaf.cadi.aaf.v2_0.AAFCon.GetSetter;
39 import org.onap.aaf.cadi.client.Future;
40 import org.onap.aaf.cadi.client.Rcli;
41 import org.onap.aaf.cadi.client.Result;
42 import org.onap.aaf.cadi.client.Retryable;
43 import org.onap.aaf.cadi.config.Config;
44 import org.onap.aaf.cadi.persist.Persist.Loader;
45 import org.onap.aaf.cadi.principal.Kind;
46 import org.onap.aaf.cadi.util.FQI;
47 import org.onap.aaf.misc.env.APIException;
48 import org.onap.aaf.misc.rosetta.env.RosettaDF;
49
50 import aafoauth.v2_0.Introspect;
51 import aafoauth.v2_0.Token;
52
53 public class TokenClient {
54         private static final String UTF_8 = "UTF-8";
55
56         public enum AUTHN_METHOD {client_credentials,password,payload,basic_auth,certificate,refresh_token, none}
57
58         private final TokenClientFactory factory;
59         private final AAFCon<?> tkCon;
60         private static RosettaDF<Token> tokenDF;
61         protected static RosettaDF<Introspect> introspectDF;
62
63
64         private int timeout;
65         private String client_id, username;
66         private byte[] enc_client_secret, enc_password;
67
68         private GetSetter ss;
69         private AUTHN_METHOD authn_method;
70         private byte[] hash;
71         private final char okind;
72         private String default_scope;
73
74         // Package on Purpose
75         TokenClient(char okind, final TokenClientFactory tcf, final AAFCon<?> tkCon, final int timeout, AUTHN_METHOD am) throws CadiException, APIException {
76                 this.okind = okind;
77                 factory = tcf;
78                 this.tkCon = tkCon;
79                 this.timeout = timeout;
80                 ss = null;
81                 authn_method = am;
82                 synchronized(tcf) {
83                         if(introspectDF==null) {
84                                 tokenDF = tkCon.env().newDataFactory(Token.class);
85                                 introspectDF = tkCon.env().newDataFactory(Introspect.class);
86                         }
87                 }
88  
89         }
90
91         public void client_id(String client_id) {
92                 this.client_id = client_id;
93                 default_scope = FQI.reverseDomain(client_id);
94         }
95         
96         public String client_id() {
97                 return client_id;
98         }
99         
100         /**
101          * This scope based on client_id... the App configured for call
102          * @return
103          */
104         public String defaultScope() {
105                 return default_scope;
106         }
107
108         public void client_creds(Access access) throws CadiException {
109                 if(okind=='A') {
110                         client_creds(access.getProperty(Config.AAF_APPID, null),access.getProperty(Config.AAF_APPPASS, null));
111                 } else {
112                         client_creds(access.getProperty(Config.AAF_ALT_CLIENT_ID, null),access.getProperty(Config.AAF_ALT_CLIENT_SECRET, null));
113                 }
114         }
115
116         /**
117          * Note: OAuth2 provides for normal Authentication parameters when getting tokens.  Basic Auth is one such valid
118          * way to get Credentials.  However, support is up to the OAuth2 Implementation
119          * 
120          * This method is for setting an App's creds (client) to another App.
121          * 
122          * @param client_id
123          * @param client_secret
124          * @throws IOException
125          */
126         public void client_creds(final String client_id, final String client_secret) throws CadiException {
127                 if(client_id==null) {
128                         throw new CadiException(Config.AAF_ALT_CLIENT_ID + " is null");
129                 }
130                 this.client_id = client_id;
131                 default_scope = FQI.reverseDomain(client_id);
132
133                 if(client_secret!=null) {
134                         try {
135                                 if(client_secret.startsWith("enc:")) {
136                                         final String temp = factory.access.decrypt(client_secret, false); // this is a more powerful, but non-thread-safe encryption
137                                         hash = Hash.hashSHA256(temp.getBytes());
138                                         this.enc_client_secret = factory.symm.encode(temp.getBytes());
139                                         ss = new GetSetter() {
140                                                 @Override
141                                                 public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
142                                                         return con.basicAuth(client_id, temp);// Base class encrypts password
143                                                 }
144                                         };
145                                 } else {
146                                         byte[] temp = client_secret.getBytes();
147                                         hash = Hash.hashSHA256(temp);
148                                         this.enc_client_secret = factory.symm.encode(temp);
149                                         ss = new GetSetter() {
150                                                 @Override
151                                                 public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
152                                                         return con.basicAuth(client_id, client_secret);// Base class encrypts password
153                                                 }
154                                         };
155                                 }
156                                 authn_method = AUTHN_METHOD.client_credentials;
157                         } catch(IOException | NoSuchAlgorithmException e) {
158                                 throw new CadiException(e);
159                         }
160                 } else {
161                         ss = new GetSetter() {
162                                 @Override
163                                 public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
164                                         try {
165                                                 return con.x509Alias(client_id);// no password, assume Cert
166                                         } catch (APIException e) {
167                                                 throw new CadiException(e);
168                                         } 
169                                 }                               
170                         };
171                         authn_method = AUTHN_METHOD.client_credentials;
172                 }
173         }
174         
175         public void username(String username) {
176                 this.username = username;
177         }
178
179         /**
180          * Note: OAuth2 provides for normal Authentication parameters when getting tokens.  Basic Auth is one such valid
181          * way to get Credentials.  However, support is up to the OAuth2 Implementation
182          * 
183          * This method is for setting the End-User's Creds
184          * 
185          * @param client_id
186          * @param client_secret
187          * @throws IOException
188          */
189         public void password(final String user, final String password) throws CadiException {
190                 this.username = user;
191                 if(password!=null) {
192                         try {
193                                 if(password.startsWith("enc:")) {
194                                         final String temp = factory.access.decrypt(password, false); // this is a more powerful, but non-thread-safe encryption
195                                         hash = Hash.hashSHA256(temp.getBytes());
196                                         this.enc_password = factory.symm.encode(temp.getBytes());
197                                         ss = new GetSetter() {
198                                                 @Override
199                                                 public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
200                                                         return con.basicAuth(user, temp);// Base class encrypts password
201                                                 }
202                                         };
203                                 } else {
204                                         byte[] temp = password.getBytes();
205                                         hash = Hash.hashSHA256(temp);
206                                         this.enc_password = factory.symm.encode(temp);
207                                         ss = new GetSetter() {
208                                                 @Override
209                                                 public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
210                                                         return con.basicAuth(user, password);// Base class encrypts password
211                                                 }
212                                         };
213                                 }
214                                 authn_method = AUTHN_METHOD.password;
215                         } catch (IOException | NoSuchAlgorithmException e) {
216                                 throw new CadiException(e);
217                         }
218                 }
219         }
220         
221         public void clearEndUser() {
222                 username = null;
223                 enc_password = null;
224                 if(client_id!=null && enc_client_secret!=null) {
225                         authn_method = AUTHN_METHOD.client_credentials;
226                 } else {
227                         authn_method = AUTHN_METHOD.password;
228                 }
229         }
230
231         public Result<TimedToken> getToken(final String ... scopes) throws LocatorException, CadiException, APIException {
232                 return getToken(Kind.OAUTH,scopes);
233         }
234
235         public void clearToken(final String ... scopes) throws CadiException {
236                 clearToken(Kind.OAUTH,scopes);
237         }
238
239         public void clearToken(final char kind, final String ... scopes) throws CadiException {
240                 final String scope = addScope(scopes);
241                 char c;
242                 if(kind==Kind.OAUTH) {
243                         c = okind;
244                 } else {
245                         c = kind;
246                 }
247                 final String key = TokenClientFactory.getKey(c,client_id,username,hash,scope);
248                 factory.delete(key);
249         }
250         /**
251          * Get AuthToken
252          * @throws APIException 
253          * @throws CadiException 
254          * @throws LocatorException 
255          */
256         public Result<TimedToken> getToken(final char kind, final String ... scopes) throws LocatorException, CadiException, APIException {
257                 final String scope = addScope(scopes);
258                 char c;
259                 if(kind==Kind.OAUTH) {
260                         c = okind;
261                 } else {
262                         c = kind;
263                 }
264                 final String key = TokenClientFactory.getKey(c,client_id,username,hash,scope);
265                 if(ss==null) {
266                         throw new APIException("client_creds(...) must be set before obtaining Access Tokens");
267                 }
268                 
269                 Result<TimedToken> rtt = factory.get(key,hash,new Loader<TimedToken>() {
270                         @Override
271                         public Result<TimedToken> load(final String key) throws APIException, CadiException, LocatorException {
272                                 final List<String> params = new ArrayList<String>();
273                                 params.add(scope);
274                                 addSecurity(params,authn_method);
275                         
276                                 final String paramsa[] = new String[params.size()];
277                                 params.toArray(paramsa);
278                                 Result<Token> rt = tkCon.best(new Retryable<Result<Token>>() {
279                                         @Override
280                                         public Result<Token> code(Rcli<?> client) throws CadiException, ConnectException, APIException {
281                                                 // /token?grant_type=client_credential&scope=com.att.aaf+com.att.test
282                                                 Future<Token> f = client.postForm(null,tokenDF,paramsa);
283                                                 if(f.get(timeout)) {
284                                                         return Result.ok(f.code(),f.value);
285                                                 } else {
286                                                         return Result.err(f.code(), f.body());
287                                                 }
288                                         }
289                                 });
290                                 
291                                 if(rt.isOK()) {
292                                         try {
293                                                 return Result.ok(rt.code,factory.putTimedToken(key,rt.value, hash));
294                                         } catch (IOException e) {
295                                                 // TODO What to do here?
296                                                 e.printStackTrace();
297                                                 return Result.err(999,e.getMessage());
298                                         }
299                                 } else {
300                                         return Result.err(rt);
301                                 }
302                         }
303                 });
304                 if(rtt.isOK()) { // not validated for Expired
305                         TimedToken tt = rtt.value;
306                         if(tt.expired()) {
307                                 rtt = refreshToken(tt);
308                                 if(rtt.isOK()) {
309                                         tkCon.access.printf(Level.INFO, "Refreshed token %s to %s",tt.getAccessToken(),rtt.value.getAccessToken());
310                                         return Result.ok(200,rtt.value);
311                                 } else {
312                                         tkCon.access.printf(Level.INFO, "Expired token %s cannot be renewed %d %s",tt.getAccessToken(),rtt.code,rtt.error);
313                                         factory.delete(key);
314                                         tt=null;
315                                 }
316                         } else {
317                                 return Result.ok(200,tt);
318                         }
319                 } else {
320                         Result.err(rtt);
321                 }
322                 return Result.err(404,"Not Found");
323         }
324         
325         public Result<TimedToken> refreshToken(Token token) throws APIException, LocatorException, CadiException {
326                 if(ss==null) {
327                         throw new APIException("client_creds(...) must be set before obtaining Access Tokens");
328                 }
329                 final List<String> params = new ArrayList<String>();
330                 params.add("refresh_token="+token.getRefreshToken());
331                 addSecurity(params,AUTHN_METHOD.refresh_token);
332                 final String scope="scope="+token.getScope().replace(' ', '+');
333                 params.add(scope);
334         
335                 final String paramsa[] = new String[params.size()];
336                 params.toArray(paramsa);
337                 Result<Token> rt = tkCon.best(new Retryable<Result<Token>>() {
338                         @Override
339                         public Result<Token> code(Rcli<?> client) throws CadiException, ConnectException, APIException {
340                                 // /token?grant_type=client_credential&scope=com.att.aaf+com.att.test
341                                 Future<Token> f = client.postForm(null,tokenDF,paramsa);
342                                 if(f.get(timeout)) {
343                                         return Result.ok(f.code(),f.value);
344                                 } else {
345                                         return Result.err(f.code(), f.body());
346                                 }
347                         }
348                 });
349                 String key =  TokenClientFactory.getKey(okind,client_id, username, hash, scope);
350                 if(rt.isOK()) {
351                         try {
352                                 return Result.ok(200,factory.putTimedToken(key, rt.value, hash));
353                         } catch (IOException e) {
354                                 //TODO what to do here?
355                                 return Result.err(999, e.getMessage());
356                         }
357                 } else if(rt.code==404) {
358                         factory.deleteFromDisk(key);
359                 }
360                 return Result.err(rt);
361         }
362
363         public Result<Introspect> introspect(final String token) throws APIException, LocatorException, CadiException {
364                 if(ss==null) {
365                         throw new APIException("client_creds(...) must be set before introspecting Access Tokens");
366                 }
367
368                 return tkCon.best(new Retryable<Result<Introspect>>() {
369                                 @Override
370                                 public Result<Introspect> code(Rcli<?> client) throws CadiException, ConnectException, APIException {
371                                         final List<String> params = new ArrayList<String>();
372                                         params.add("token="+token);
373                                         addSecurity(params,AUTHN_METHOD.client_credentials);
374                                         final String paramsa[] = new String[params.size()];
375                                         params.toArray(paramsa);
376                                         // /token?grant_type=client_credential&scope=com.att.aaf+com.att.test
377                                         Future<Introspect> f = client.postForm(null,introspectDF,paramsa);
378                                         if(f.get(timeout)) {
379                                                 return Result.ok(f.code(),f.value);
380                                         } else {
381                                                 return Result.err(f.code(), f.body());
382                                         }
383                                 }
384                         }
385                 );
386         }
387         
388         private String addScope(String[] scopes) {
389                 String rv = null;
390                 StringBuilder scope=null;
391                 boolean first = true;
392                 for(String s : scopes) {
393                         if(first) {
394                                 scope = new StringBuilder();
395                                 scope.append("scope=");
396                                 first=false;
397                         } else {
398                                 scope.append('+');
399                         }
400                         scope.append(s);
401                 }
402                 if(scope!=null) {
403                         rv=scope.toString();
404                 }
405                 return rv;
406         }
407
408         private void addSecurity(List<String> params, AUTHN_METHOD authn) throws APIException {
409                 // Set GrantType... different than Credentials
410                 switch(authn) {
411                         case client_credentials:
412                                 params.add("grant_type=client_credentials");
413                                 break;
414                         case password:
415                                 params.add("grant_type=password");
416                                 break;
417                         case refresh_token:
418                                 params.add("grant_type=refresh_token");
419                                 break;
420                         case none:
421                                 break;
422                         default:
423                                 // Nothing to do
424                                 break;
425                 }
426                 
427                 // Set Credentials appropriate 
428                 switch(authn_method) {
429                         case client_credentials:
430                                 if(client_id!=null) {
431                                         params.add("client_id="+client_id);
432                                 }
433                 
434                                 if(enc_client_secret!=null) {
435                                         try {
436                                                 params.add("client_secret="+URLEncoder.encode(new String(factory.symm.decode(enc_client_secret)),UTF_8));
437                                         } catch (IOException e) {
438                                                 throw new APIException("Error Decrypting Password",e);
439                                         }
440                                 }
441                                 break;
442                         case refresh_token:
443                                 if(client_id!=null) {
444                                         params.add("client_id="+client_id);
445                                 }
446                 
447                                 if(enc_client_secret!=null) {
448                                         try {
449                                                 params.add("client_secret="+URLEncoder.encode(new String(factory.symm.decode(enc_client_secret)),UTF_8));
450                                         } catch (IOException e) {
451                                                 throw new APIException("Error Decrypting Password",e);
452                                         }
453                                 }
454                                 break;
455
456                         case password:
457                                 if(client_id!=null) {
458                                         params.add("client_id="+client_id);
459                                 }
460                 
461                                 if(enc_client_secret!=null) {
462                                         try {
463                                                 params.add("client_secret="+ URLEncoder.encode(new String(factory.symm.decode(enc_client_secret)),UTF_8));
464                                         } catch (IOException e) {
465                                                 throw new APIException("Error Decrypting Password",e);
466                                         }
467                                 }
468                                 if(username!=null) {
469                                         params.add("username="+username);
470                                 }
471                 
472                                 if(enc_password!=null) {
473                                         try {
474                                                 params.add("password="+ URLEncoder.encode(new String(factory.symm.decode(enc_password)),UTF_8));
475                                         } catch (IOException e) {
476                                                 throw new APIException("Error Decrypting Password",e);
477                                         }
478                                 }
479         
480                                 break;
481                         default:
482                                 // Nothing to do
483                                 break;
484                 }
485         }
486 }