58ecaa84eb4b99c5b95953a5956bae6543969da3
[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             String alias = access.getProperty(Config.CADI_ALIAS, null);
111             if (alias == null) {
112                 client_creds(access.getProperty(Config.AAF_APPID, null),access.getProperty(Config.AAF_APPPASS, null));
113             } else {
114                 client_creds(alias,null);
115             }
116         } else {
117             client_creds(access.getProperty(Config.AAF_ALT_CLIENT_ID, null),access.getProperty(Config.AAF_ALT_CLIENT_SECRET, null));
118         }
119     }
120
121     /**
122      * Note: OAuth2 provides for normal Authentication parameters when getting tokens.  Basic Auth is one such valid
123      * way to get Credentials.  However, support is up to the OAuth2 Implementation
124      * 
125      * This method is for setting an App's creds (client) to another App.
126      * 
127      * @param client_id
128      * @param client_secret
129      * @throws IOException
130      */
131     public void client_creds(final String client_id, final String client_secret) throws CadiException {
132         if (client_id==null) {
133             throw new CadiException("client_creds:client_id is null");
134         }
135         this.client_id = client_id;
136         default_scope = FQI.reverseDomain(client_id);
137
138         if (client_secret!=null) {
139             try {
140                 if (client_secret.startsWith("enc:")) {
141                     final String temp = factory.access.decrypt(client_secret, false); // this is a more powerful, but non-thread-safe encryption
142                     hash = Hash.hashSHA256(temp.getBytes());
143                     this.enc_client_secret = factory.symm.encode(temp.getBytes());
144                     ss = new GetSetter() {
145                         @Override
146                         public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
147                             return con.basicAuth(client_id, temp);// Base class encrypts password
148                         }
149                     };
150                 } else {
151                     byte[] temp = client_secret.getBytes();
152                     hash = Hash.hashSHA256(temp);
153                     this.enc_client_secret = factory.symm.encode(temp);
154                     ss = new GetSetter() {
155                         @Override
156                         public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
157                             return con.basicAuth(client_id, client_secret);// Base class encrypts password
158                         }
159                     };
160                 }
161                 authn_method = AUTHN_METHOD.client_credentials;
162             } catch (IOException | NoSuchAlgorithmException e) {
163                 throw new CadiException(e);
164             }
165         } else {
166             ss = new GetSetter() {
167                 @Override
168                 public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
169                     try {
170                         return con.x509Alias(client_id);// no password, assume Cert
171                     } catch (APIException e) {
172                         throw new CadiException(e);
173                     } 
174                 }                
175             };
176             authn_method = AUTHN_METHOD.client_credentials;
177         }
178     }
179     
180     public void username(String username) {
181         this.username = username;
182     }
183
184     /**
185      * Note: OAuth2 provides for normal Authentication parameters when getting tokens.  Basic Auth is one such valid
186      * way to get Credentials.  However, support is up to the OAuth2 Implementation
187      * 
188      * This method is for setting the End-User's Creds
189      * 
190      * @param client_id
191      * @param client_secret
192      * @throws IOException
193      */
194     public void password(final String user, final String password) throws CadiException {
195         this.username = user;
196         if (password!=null) {
197             try {
198                 if (password.startsWith("enc:")) {
199                     final String temp = factory.access.decrypt(password, false); // this is a more powerful, but non-thread-safe encryption
200                     hash = Hash.hashSHA256(temp.getBytes());
201                     this.enc_password = factory.symm.encode(temp.getBytes());
202                     ss = new GetSetter() {
203                         @Override
204                         public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
205                             return con.basicAuth(user, temp);// Base class encrypts password
206                         }
207                     };
208                 } else {
209                     byte[] temp = password.getBytes();
210                     hash = Hash.hashSHA256(temp);
211                     this.enc_password = factory.symm.encode(temp);
212                     ss = new GetSetter() {
213                         @Override
214                         public <CLIENT> SecuritySetter<CLIENT> get(AAFCon<CLIENT> con) throws CadiException {
215                             return con.basicAuth(user, password);// Base class encrypts password
216                         }
217                     };
218                 }
219                 authn_method = AUTHN_METHOD.password;
220             } catch (IOException | NoSuchAlgorithmException e) {
221                 throw new CadiException(e);
222             }
223         }
224     }
225     
226     public void clearEndUser() {
227         username = null;
228         enc_password = null;
229         if (client_id!=null && enc_client_secret!=null) {
230             authn_method = AUTHN_METHOD.client_credentials;
231         } else {
232             authn_method = AUTHN_METHOD.password;
233         }
234     }
235
236     public Result<TimedToken> getToken(final String ... scopes) throws LocatorException, CadiException, APIException {
237         return getToken(Kind.OAUTH,scopes);
238     }
239
240     public void clearToken(final String ... scopes) throws CadiException {
241         clearToken(Kind.OAUTH,scopes);
242     }
243
244     public void clearToken(final char kind, final String ... scopes) throws CadiException {
245         final String scope = addScope(scopes);
246         char c;
247         if (kind==Kind.OAUTH) {
248             c = okind;
249         } else {
250             c = kind;
251         }
252         final String key = TokenClientFactory.getKey(c,client_id,username,hash,scope);
253         factory.delete(key);
254     }
255     /**
256      * Get AuthToken
257      * @throws APIException 
258      * @throws CadiException 
259      * @throws LocatorException 
260      */
261     public Result<TimedToken> getToken(final char kind, final String ... scopes) throws LocatorException, CadiException, APIException {
262         final String scope = addScope(scopes);
263         char c;
264         if (kind==Kind.OAUTH) {
265             c = okind;
266         } else {
267             c = kind;
268         }
269         final String key = TokenClientFactory.getKey(c,client_id,username,hash,scope);
270         if (ss==null) {
271             throw new APIException("client_creds(...) must be set before obtaining Access Tokens");
272         }
273         
274         Result<TimedToken> rtt = factory.get(key,hash,new Loader<TimedToken>() {
275             @Override
276             public Result<TimedToken> load(final String key) throws APIException, CadiException, LocatorException {
277                 final List<String> params = new ArrayList<>();
278                 params.add(scope);
279                 addSecurity(params,authn_method);
280             
281                 final String paramsa[] = new String[params.size()];
282                 params.toArray(paramsa);
283                 Result<Token> rt = tkCon.best(new Retryable<Result<Token>>() {
284                     @Override
285                     public Result<Token> code(Rcli<?> client) throws CadiException, ConnectException, APIException {
286                         // /token?grant_type=client_credential&scope=com.att.aaf+com.att.test
287                         Future<Token> f = client.postForm(null,tokenDF,paramsa);
288                         if (f.get(timeout)) {
289                             return Result.ok(f.code(),f.value);
290                         } else {
291                             return Result.err(f.code(), f.body());
292                         }
293                     }
294                 });
295                 
296                 if (rt.isOK()) {
297                     try {
298                         return Result.ok(rt.code,factory.putTimedToken(key,rt.value, hash));
299                     } catch (IOException e) {
300                         // TODO What to do here?
301                         e.printStackTrace();
302                         return Result.err(999,e.getMessage());
303                     }
304                 } else {
305                     return Result.err(rt);
306                 }
307             }
308         });
309         if (rtt.isOK()) { // not validated for Expired
310             TimedToken tt = rtt.value;
311             if (tt.expired()) {
312                 rtt = refreshToken(tt);
313                 if (rtt.isOK()) {
314                     tkCon.access.printf(Level.INFO, "Refreshed token %s to %s",tt.getAccessToken(),rtt.value.getAccessToken());
315                     return Result.ok(200,rtt.value);
316                 } else {
317                     tkCon.access.printf(Level.INFO, "Expired token %s cannot be renewed %d %s",tt.getAccessToken(),rtt.code,rtt.error);
318                     factory.delete(key);
319                     tt=null;
320                 }
321             } else {
322                 return Result.ok(200,tt);
323             }
324         } else {
325             Result.err(rtt);
326         }
327         return Result.err(404,"Not Found");
328     }
329     
330     public Result<TimedToken> refreshToken(Token token) throws APIException, LocatorException, CadiException {
331         if (ss==null) {
332             throw new APIException("client_creds(...) must be set before obtaining Access Tokens");
333         }
334         final List<String> params = new ArrayList<>();
335         params.add("refresh_token="+token.getRefreshToken());
336         addSecurity(params,AUTHN_METHOD.refresh_token);
337         final String scope="scope="+token.getScope().replace(' ', '+');
338         params.add(scope);
339     
340         final String paramsa[] = new String[params.size()];
341         params.toArray(paramsa);
342         Result<Token> rt = tkCon.best(new Retryable<Result<Token>>() {
343             @Override
344             public Result<Token> code(Rcli<?> client) throws CadiException, ConnectException, APIException {
345                 // /token?grant_type=client_credential&scope=com.att.aaf+com.att.test
346                 Future<Token> f = client.postForm(null,tokenDF,paramsa);
347                 if (f.get(timeout)) {
348                     return Result.ok(f.code(),f.value);
349                 } else {
350                     return Result.err(f.code(), f.body());
351                 }
352             }
353         });
354         String key =  TokenClientFactory.getKey(okind,client_id, username, hash, scope);
355         if (rt.isOK()) {
356             try {
357                 return Result.ok(200,factory.putTimedToken(key, rt.value, hash));
358             } catch (IOException e) {
359                 //TODO what to do here?
360                 return Result.err(999, e.getMessage());
361             }
362         } else if (rt.code==404) {
363             factory.deleteFromDisk(key);
364         }
365         return Result.err(rt);
366     }
367
368     public Result<Introspect> introspect(final String token) throws APIException, LocatorException, CadiException {
369         if (ss==null) {
370             throw new APIException("client_creds(...) must be set before introspecting Access Tokens");
371         }
372
373         return tkCon.best(new Retryable<Result<Introspect>>() {
374                 @Override
375                 public Result<Introspect> code(Rcli<?> client) throws CadiException, ConnectException, APIException {
376                     final List<String> params = new ArrayList<>();
377                     params.add("token="+token);
378                     addSecurity(params,AUTHN_METHOD.client_credentials);
379                     final String paramsa[] = new String[params.size()];
380                     params.toArray(paramsa);
381                     // /token?grant_type=client_credential&scope=com.att.aaf+com.att.test
382                     Future<Introspect> f = client.postForm(null,introspectDF,paramsa);
383                     if (f.get(timeout)) {
384                         return Result.ok(f.code(),f.value);
385                     } else {
386                         return Result.err(f.code(), f.body());
387                     }
388                 }
389             }
390         );
391     }
392     
393     private String addScope(String[] scopes) {
394         String rv = null;
395         StringBuilder scope=null;
396         boolean first = true;
397         for (String s : scopes) {
398             if (first) {
399                 scope = new StringBuilder();
400                 scope.append("scope=");
401                 first=false;
402             } else {
403                 scope.append('+');
404             }
405             scope.append(s);
406         }
407         if (scope!=null) {
408             rv=scope.toString();
409         }
410         return rv;
411     }
412
413     private void addSecurity(List<String> params, AUTHN_METHOD authn) throws APIException {
414         // Set GrantType... different than Credentials
415         switch(authn) {
416             case client_credentials:
417                 params.add("grant_type=client_credentials");
418                 break;
419             case password:
420                 params.add("grant_type=password");
421                 break;
422             case refresh_token:
423                 params.add("grant_type=refresh_token");
424                 break;
425             case none:
426                 break;
427             default:
428                 // Nothing to do
429                 break;
430         }
431         
432         // Set Credentials appropriate 
433         switch(authn_method) {
434             case client_credentials:
435                 if (client_id!=null) {
436                     params.add("client_id="+client_id);
437                 }
438         
439                 if (enc_client_secret!=null) {
440                     try {
441                         params.add("client_secret="+URLEncoder.encode(new String(factory.symm.decode(enc_client_secret)),UTF_8));
442                     } catch (IOException e) {
443                         throw new APIException("Error Decrypting Password",e);
444                     }
445                 }
446                 
447                 if (username!=null) {
448                     params.add("username="+username);
449                 }
450
451                 break;
452             case refresh_token:
453                 if (client_id!=null) {
454                     params.add("client_id="+client_id);
455                 }
456         
457                 if (enc_client_secret!=null) {
458                     try {
459                         params.add("client_secret="+URLEncoder.encode(new String(factory.symm.decode(enc_client_secret)),UTF_8));
460                     } catch (IOException e) {
461                         throw new APIException("Error Decrypting Password",e);
462                     }
463                 }
464                 break;
465
466             case password:
467                 if (client_id!=null) {
468                     params.add("client_id="+client_id);
469                 }
470         
471                 if (enc_client_secret!=null) {
472                     try {
473                         params.add("client_secret="+ URLEncoder.encode(new String(factory.symm.decode(enc_client_secret)),UTF_8));
474                     } catch (IOException e) {
475                         throw new APIException("Error Decrypting Password",e);
476                     }
477                 }
478                 if (username!=null) {
479                     params.add("username="+username);
480                 }
481         
482                 if (enc_password!=null) {
483                     try {
484                         params.add("password="+ URLEncoder.encode(new String(factory.symm.decode(enc_password)),UTF_8));
485                     } catch (IOException e) {
486                         throw new APIException("Error Decrypting Password",e);
487                     }
488                 }
489     
490                 break;
491             default:
492                 // Nothing to do
493                 break;
494         }
495     }
496 }