package divconq.service.db; import java.net.URL; import java.net.URLEncoder; import java.util.function.Consumer; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.HttpsURLConnection; import divconq.bus.IService; import divconq.bus.Message; import divconq.db.DataRequest; import divconq.db.IDatabaseManager; import divconq.db.ObjectFinalResult; import divconq.db.ObjectResult; import divconq.db.common.RequestFactory; import divconq.db.query.LoadRecordRequest; import divconq.db.query.SelectDirectRequest; import divconq.db.query.SelectFields; import divconq.db.query.WhereEqual; import divconq.db.query.WhereField; import divconq.db.update.InsertRecordRequest; import divconq.db.update.UpdateRecordRequest; import divconq.hub.Hub; import divconq.lang.op.FuncResult; import divconq.lang.op.OperationContext; import divconq.lang.op.OperationContextBuilder; import divconq.lang.op.UserContext; import divconq.mod.ExtensionBase; import divconq.struct.CompositeParser; import divconq.struct.CompositeStruct; import divconq.struct.FieldStruct; import divconq.struct.ListStruct; import divconq.struct.RecordStruct; import divconq.util.HexUtil; import divconq.util.StringUtil; import divconq.work.TaskRun; public class AuthService extends ExtensionBase implements IService { @Override public void handle(TaskRun request) { Message msg = (Message) request.getTask().getParams(); String feature = msg.getFieldAsString("Feature"); String op = msg.getFieldAsString("Op"); final OperationContext tc = OperationContext.get(); final UserContext uc = tc.getUserContext(); IDatabaseManager db = Hub.instance.getDatabase(); if (db == null) { request.errorTr(303); request.complete(); return; } //System.out.println("Auth: " + feature + " - " + op); if ("Facebook".equals(feature)) { if ("LinkAccount".equals(op)) { // try to authenticate RecordStruct creds = msg.getFieldAsRecord("Body"); String fbtoken = creds.getFieldAsString("AccessToken"); RecordStruct fbinfo = AuthService.fbSignIn(fbtoken, null); // TODO use FB secret key someday? for app proof... if (request.hasErrors() || (fbinfo == null)) { AuthService.this.clearUserContext(OperationContext.get()); request.errorTr(442); request.complete(); return; } // TODO allow only `verified` fb users? if (fbinfo.isFieldEmpty("id") || fbinfo.isFieldEmpty("email") || fbinfo.isFieldEmpty("first_name") || fbinfo.isFieldEmpty("last_name")) { AuthService.this.clearUserContext(OperationContext.get()); request.errorTr(442); request.complete(); return; } String uid = fbinfo.getFieldAsString("id"); UpdateRecordRequest req = new UpdateRecordRequest(); req .withTable("dcUser") .withId(OperationContext.get().getUserContext().getUserId()) .withUpdateField("dcmFacebookId", uid); db.submit(req, new ObjectFinalResult(request) ); return; } } else if ("Authentication".equals(feature)) { if ("SignIn".equals(op)) { LoadRecordRequest req = new LoadRecordRequest() .withTable("dcUser") .withId(uc.getUserId()) .withNow() .withSelect(new SelectFields() .withField("dcUsername", "Username") .withField("dcFirstName", "FirstName") .withField("dcLastName", "LastName") .withField("dcEmail", "Email") ); db.submit(req, new ObjectResult() { @Override public void process(CompositeStruct result) { if (request.hasErrors() || (result == null)) { AuthService.this.clearUserContext(request.getContext()); request.errorTr(442); } request.returnValue(result); } }); return; } if ("SignInFacebook".equals(op)) { // TODO check domain settings that FB sign in is allowed // try to authenticate RecordStruct creds = msg.getFieldAsRecord("Body"); //String uid = creds.getFieldAsString("UserId"); String fbtoken = creds.getFieldAsString("AccessToken"); RecordStruct fbinfo = AuthService.fbSignIn(fbtoken, null); // TODO use FB secret key someday? for app proof... if (request.hasErrors() || (fbinfo == null)) { AuthService.this.clearUserContext(OperationContext.get()); request.errorTr(442); request.complete(); return; } // TODO allow only `verified` fb users? if (fbinfo.isFieldEmpty("id") || fbinfo.isFieldEmpty("email") || fbinfo.isFieldEmpty("first_name") || fbinfo.isFieldEmpty("last_name")) { AuthService.this.clearUserContext(OperationContext.get()); request.errorTr(442); request.complete(); return; } String uid = fbinfo.getFieldAsString("id"); // sigin callback Consumer<String> signincb = new Consumer<String>() { @Override public void accept(String userid) { DataRequest tp1 = RequestFactory.startSessionRequest(userid); // TODO for all services, be sure we return all messages from the submit with the message db.submit(tp1, new ObjectResult() { @Override public void process(CompositeStruct result) { RecordStruct sirec = (RecordStruct) result; OperationContext ctx = request.getContext(); //System.out.println("auth 2: " + request.getContext().isElevated()); if (request.hasErrors() || (sirec == null)) { AuthService.this.clearUserContext(ctx); request.errorTr(442); request.complete(); return; } ListStruct atags = sirec.getFieldAsList("AuthorizationTags"); atags.addItem("User"); // TODO make locale smart String fullname = ""; if (!sirec.isFieldEmpty("FirstName")) fullname = sirec.getFieldAsString("FirstName"); if (!sirec.isFieldEmpty("LastName") && StringUtil.isNotEmpty(fullname)) fullname += " " + sirec.getFieldAsString("LastName"); else if (!sirec.isFieldEmpty("LastName")) fullname = sirec.getFieldAsString("LastName"); if (StringUtil.isEmpty(fullname)) fullname = "[unknown]"; OperationContext.switchUser(ctx, ctx.getUserContext().toBuilder() .withVerified(true) .withAuthToken(sirec.getFieldAsString("AuthToken")) .withUserId(sirec.getFieldAsString("UserId")) .withUsername(sirec.getFieldAsString("Username")) .withFullName(fullname) .withEmail(sirec.getFieldAsString("Email")) .withAuthTags(atags) .toUserContext() ); Hub.instance.getSessions().findOrCreateTether(request.getContext()); request.returnValue(new RecordStruct( new FieldStruct("Username", sirec.getFieldAsString("Username")), new FieldStruct("FirstName", sirec.getFieldAsString("FirstName")), new FieldStruct("LastName", sirec.getFieldAsString("LastName")), new FieldStruct("Email", sirec.getFieldAsString("Email")) )); } }); } }; // ----------------------------------------- // find user - update or insert user record // ----------------------------------------- db.submit( new SelectDirectRequest() .withTable("dcUser") .withSelect(new SelectFields() .withField("Id") .withField("dcUsername", "Username") .withField("dcFirstName", "FirstName") .withField("dcLastName", "LastName") .withField("dcEmail", "Email") ) .withWhere( new WhereEqual(new WhereField("dcmFacebookId"), uid) // TODO or where `username` = `fb email` - maybe? ), new ObjectResult() { @Override public void process(CompositeStruct uLookupResult) { if (this.hasErrors() || (uLookupResult == null)) { request.error("Error finding user record"); request.complete(); return; } ListStruct ulLookupResult = (ListStruct) uLookupResult; if (ulLookupResult.getSize() == 0) { // insert new user record InsertRecordRequest req = new InsertRecordRequest(); req .withTable("dcUser") .withSetField("dcUsername", fbinfo.getFieldAsString("email")) .withSetField("dcEmail", fbinfo.getFieldAsString("email")) .withSetField("dcFirstName", fbinfo.getFieldAsString("first_name")) .withSetField("dcLastName", fbinfo.getFieldAsString("last_name")) .withSetField("dcmFacebookId", uid) .withSetField("dcConfirmed", true); // TODO look at fb `locale` and `timezone` too db.submit(req, new ObjectResult() { @Override public void process(CompositeStruct result) { if (this.hasErrors()) request.complete(); else signincb.accept(((RecordStruct)result).getFieldAsString("Id")); } }); } else { String dcuid = ulLookupResult.getItemAsRecord(0).getFieldAsString("Id"); UpdateRecordRequest req = new UpdateRecordRequest(); req .withTable("dcUser") .withId(dcuid) // TODO add these once UpdateField works with Dynamic Scalar //.withUpdateField("dcUsername", fbinfo.getFieldAsString("email")) //.withUpdateField("dcEmail", fbinfo.getFieldAsString("email")) //.withUpdateField("dcFirstName", fbinfo.getFieldAsString("first_name")) //.withUpdateField("dcLastName", fbinfo.getFieldAsString("last_name")) .withUpdateField("dcmFacebookId", uid) .withUpdateField("dcConfirmed", true); // TODO look at fb `locale` and `timezone` too db.submit(req, new ObjectResult() { @Override public void process(CompositeStruct result) { if (this.hasErrors()) request.complete(); else signincb.accept(dcuid); } }); } } } ); return; } // TODO now that we trust the token in Session this won't get called often - think about how to keep // auth token fresh in database - especially since the token will expire in 30 minutes if ("Verify".equals(op)) { String authToken = uc.getAuthToken(); if (StringUtil.isNotEmpty(authToken)) { DataRequest tp1 = RequestFactory.verifySessionRequest(uc.getUserId(), uc.getAuthToken()); db.submit(tp1, new ObjectResult() { public void process(CompositeStruct result) { RecordStruct urec = (RecordStruct) result; OperationContext ctx = request.getContext(); if (request.hasErrors() || (urec == null)) { AuthService.this.clearUserContext(ctx); request.errorTr(442); } else { //System.out.println("verify existing"); ListStruct atags = urec.getFieldAsList("AuthorizationTags"); atags.addItem("User"); // TODO make locale smart String fullname = ""; if (!urec.isFieldEmpty("FirstName")) fullname = urec.getFieldAsString("FirstName"); if (!urec.isFieldEmpty("LastName") && StringUtil.isNotEmpty(fullname)) fullname += " " + urec.getFieldAsString("LastName"); else if (!urec.isFieldEmpty("LastName")) fullname = urec.getFieldAsString("LastName"); if (StringUtil.isEmpty(fullname)) fullname = "[unknown]"; OperationContext.switchUser(ctx, ctx.getUserContext().toBuilder() .withVerified(true) .withUsername(urec.getFieldAsString("Username")) .withFullName(fullname) .withEmail(urec.getFieldAsString("Email")) .withAuthTags(atags) .toUserContext() ); } request.complete(); } }); return; } // else try to authenticate RecordStruct creds = uc.getCredentials(); // msg.getFieldAsRecord("Credentials"); if (creds == null) { request.errorTr(442); request.complete(); return; } //System.out.println("auth 1: " + request.getContext().isElevated()); DataRequest tp1 = RequestFactory.signInRequest(creds.getFieldAsString("Username"), creds.getFieldAsString("Password"), creds.getFieldAsString("ClientKeyPrint")); // TODO for all services, be sure we return all messages from the submit with the message db.submit(tp1, new ObjectResult() { @Override public void process(CompositeStruct result) { RecordStruct sirec = (RecordStruct) result; OperationContext ctx = request.getContext(); //System.out.println("auth 2: " + request.getContext().isElevated()); if (request.hasErrors() || (sirec == null)) { AuthService.this.clearUserContext(ctx); request.errorTr(442); } else { //System.out.println("verify new"); ListStruct atags = sirec.getFieldAsList("AuthorizationTags"); atags.addItem("User"); // TODO make locale smart String fullname = ""; if (!sirec.isFieldEmpty("FirstName")) fullname = sirec.getFieldAsString("FirstName"); if (!sirec.isFieldEmpty("LastName") && StringUtil.isNotEmpty(fullname)) fullname += " " + sirec.getFieldAsString("LastName"); else if (!sirec.isFieldEmpty("LastName")) fullname = sirec.getFieldAsString("LastName"); if (StringUtil.isEmpty(fullname)) fullname = "[unknown]"; OperationContext.switchUser(ctx, ctx.getUserContext().toBuilder() .withVerified(true) .withAuthToken(sirec.getFieldAsString("AuthToken")) .withUserId(sirec.getFieldAsString("UserId")) .withUsername(sirec.getFieldAsString("Username")) .withFullName(fullname) .withEmail(sirec.getFieldAsString("Email")) .withAuthTags(atags) .toUserContext() ); Hub.instance.getSessions().findOrCreateTether(request.getContext()); } request.complete(); } }); return; } if ("SignOut".equals(op)) { db.submit(RequestFactory.signOutRequest(uc.getAuthToken()), new ObjectResult() { @Override public void process(CompositeStruct result) { Hub.instance.getSessions().terminate(request.getContext().getSessionId()); AuthService.this.clearUserContext(request.getContext()); request.complete(); } }); return; } } else if ("Recovery".equals(feature)) { if ("InitiateSelf".equals(op) || "InitiateAdmin".equals(op)) { String user = msg.bodyRecord().getFieldAsString("User"); DataRequest req = RequestFactory.initiateRecoveryRequest(user); db.submit(req, new ObjectResult() { @Override public void process(CompositeStruct result) { if (this.hasErrors()) { request.errorTr(442); } else { String code = ((RecordStruct)result).getFieldAsString("Code"); String email = ((RecordStruct)result).getFieldAsString("Email"); String email2 = ((RecordStruct)result).getFieldAsString("BackupEmail"); // TODO send email System.out.println("Sending recovery code: " + code + " to " + email + " and " + email2); } if ("InitiateAdmin".equals(op)) // return the code/emails to the admin request.returnValue(request); else // don't return to guest request.complete(); } }); return; } } request.errorTr(441, this.serviceName(), feature, op); request.complete(); } // be sure we keep the domain id public void clearUserContext(OperationContext ctx) { UserContext uc = ctx.getUserContext(); OperationContext.switchUser(ctx, new OperationContextBuilder() .withGuestUserTemplate() .withDomainId(uc.getDomainId()) .toUserContext()); } static public RecordStruct fbSignIn(String token, String secret) { try { URL url = null; if (StringUtil.isEmpty(secret)) { url = new URL("https://graph.facebook.com/v2.2/me?access_token=" + URLEncoder.encode(token, "UTF-8")); } else { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256")); String verify = HexUtil.bufferToHex(mac.doFinal(token.getBytes())); //System.out.println("verify: " + verify); url = new URL("https://graph.facebook.com/v2.2/me?access_token=" + URLEncoder.encode(token, "UTF-8") + "&appsecret_proof=" + URLEncoder.encode(verify, "UTF-8")); //System.out.println("url: " + url); } HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); con.setRequestProperty("User-Agent", "DivConq/1.0 (Language=Java/8)"); int responseCode = con.getResponseCode(); if (responseCode == 200) { FuncResult<CompositeStruct> res = CompositeParser.parseJson(con.getInputStream()); //System.out.println("res: " + res.getResult()); return (RecordStruct) res.getResult(); } OperationContext.get().error("FB Response Code : " + responseCode); } catch (Exception x) { OperationContext.get().error("FB error: " + x); } return null; } }