From 01d99be70ec6bde005ba6e7b58274b1b98540836 Mon Sep 17 00:00:00 2001 From: Dev Gurung Date: Sun, 6 Dec 2020 20:21:46 +0545 Subject: [PATCH 1/3] Register user with DID and other info --- hs-plugin-keycloak-ejb/pom.xml | 71 ++++- .../hypersign/api/HSResourceProvider.java | 300 +++++++++--------- .../hypersign/api/HSResourceProviderTest.java | 36 +++ 3 files changed, 253 insertions(+), 154 deletions(-) create mode 100644 hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/api/HSResourceProviderTest.java diff --git a/hs-plugin-keycloak-ejb/pom.xml b/hs-plugin-keycloak-ejb/pom.xml index 2f956f0..06f7c0d 100755 --- a/hs-plugin-keycloak-ejb/pom.xml +++ b/hs-plugin-keycloak-ejb/pom.xml @@ -79,25 +79,88 @@ json 20180130 - org.codehaus.jackson jackson-mapper-asl 1.5.0 - net.glxn.qrgen qrgen-parent 2.0 pom - - + com.google.zxing core 2.0 + + org.powermock + powermock-module-junit4 + 1.6.3 + test + + + org.powermock + powermock-api-mockito + 1.6.3 + test + + + org.mockito + mockito-core + 2.4.3 + test + + + junit + junit + 4.12 + test + + + junit + junit + 4.12 + test + + + junit + junit + 4.12 + test + + + junit + junit + 4.12 + test + + + junit + junit + 4.12 + test + + + junit + junit + 4.12 + test + + + junit + junit + 4.12 + test + + + junit + junit + 4.12 + test + diff --git a/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/api/HSResourceProvider.java b/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/api/HSResourceProvider.java index 30df23b..fe82649 100644 --- a/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/api/HSResourceProvider.java +++ b/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/api/HSResourceProvider.java @@ -20,12 +20,14 @@ import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.ServicesLogger; import org.keycloak.models.*; -import java.util.HashMap; + +import java.util.HashMap; import java.util.Map; import java.util.UUID; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; + import org.json.JSONObject; import org.keycloak.wildfly.adduser.*; import org.codehaus.jackson.map.ObjectMapper; @@ -37,6 +39,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.FileInputStream; + /** * @author Stian Thorgersen */ @@ -46,23 +49,27 @@ class HSUserModel { String challange; String userId; boolean hasLoggedIn; - public HSUserModel(String challange, String userId, boolean hasLoggedIn){ + + public HSUserModel(String challange, String userId, boolean hasLoggedIn) { this.challange = challange; this.userId = userId; this.hasLoggedIn = hasLoggedIn; } } - class FResponse{ + class FResponse { public String status; public String data; - public FResponse(){} - public FResponse(String status, String data){ + + public FResponse() { + } + + public FResponse(String status, String data) { this.status = status; this.data = data; } } - + enum Status { SUCCESS, FAIL @@ -72,37 +79,37 @@ enum Status { private static HashMap userSessionMap = new HashMap<>();// this is temporary private KeycloakSession session; private static String hsAuthServerEp = ""; - - public HSResourceProvider(KeycloakSession session) { + + public HSResourceProvider(KeycloakSession session) { this.session = session; } - private String getHsEP() throws IOException{ + private String getHsEP() throws IOException { String hsAuthServerEp = ""; FileInputStream fis = null; - try{ + try { logger.info("Inside the getHsEP constructor"); String fileName = System.getProperty("jboss.server.config.dir") + "/hypersign.properties"; logger.info(fileName); - Properties properties = new Properties(); + Properties properties = new Properties(); fis = new FileInputStream(fileName); properties.load(fis); hsAuthServerEp = properties.getProperty("auth-server-endpoint"); - if(!hsAuthServerEp.isEmpty()){ + if (!hsAuthServerEp.isEmpty()) { //add / if not there - if(hsAuthServerEp.charAt(hsAuthServerEp.length() -1) != '/'){ + if (hsAuthServerEp.charAt(hsAuthServerEp.length() - 1) != '/') { hsAuthServerEp = hsAuthServerEp + "/"; } logger.info("HS auth server endpoint configured."); logger.info(hsAuthServerEp); - }else{ + } else { logger.info("HS auth server endpoint not configured. Setting it to default"); hsAuthServerEp = "http://localhost:3000/"; logger.info(hsAuthServerEp); } - }catch(IOException e){ + } catch (IOException e) { logger.info(e.toString()); - }finally{ + } finally { fis.close(); } logger.info("Inside the getHsEP constructor ends"); @@ -134,7 +141,7 @@ public Response get() { if (name == null) { name = session.getContext().getRealm().getName(); } - return this.formattedReponse(Status.SUCCESS, "Hi, there! " + name); + return this.formattedReponse(Status.SUCCESS, "Hi, there! " + name); } @POST @@ -143,58 +150,49 @@ public Response get() { @Produces(MediaType.APPLICATION_JSON) public Response register(String body) { logger.info("Register api called!"); - JSONObject json = null; - String publicKey = ""; + JSONObject json = new JSONObject(); + String userDID = ""; String emaiid = ""; String username = ""; String companyid = ""; UserModel newuser = null; - try{ - if(!body.isEmpty()){ + try { + if (!body.isEmpty()) { json = new JSONObject(body); - if(json != null){ - publicKey = json.getString("publickey"); + if (json != null) { + userDID = json.getString("did"); emaiid = json.getString("email"); username = json.getString("username"); companyid = json.getString("companyid"); - if(!publicKey.isEmpty() || !emaiid.isEmpty()) { - //saving the user in hs-auth-server - String url = this.getHsEP() + "register"; - String responseFromAuthServer = AuthServerCaller.postApiCall(url,body); - json = new JSONObject(responseFromAuthServer); - if(json.getInt("status") == 0){ - throw new Exception(json.getString("message")); - }else{ - // I had to trim the publickey to accomodate it in ID field (which is of size 36) in db - publicKey = publicKey.substring(0, publicKey.length() - 6); - // saving the user in keycloak - UserRepresentation userRep = new UserRepresentation(); - userRep.setUsername(username); - userRep.setId(publicKey); - userRep.setEmail(emaiid); - userRep.setEnabled(true); - userRep.setEmailVerified(false); - newuser = this.session != null && this.session.getContext() != null - ? RepresentationToModel.createUser(this.session, this.session.getContext().getRealm(), userRep) - : null; - if(newuser != null){ - return this.formattedReponse(Status.SUCCESS, json.getString("message")); - }else{ - throw new Exception("Could not create the user"); - } + if (!userDID.isEmpty() || !emaiid.isEmpty()) { + // saving the user in keycloak + UserRepresentation userRep = new UserRepresentation(); + userRep.setUsername(username); + userRep.setId(userDID); + userRep.setEmail(emaiid); + userRep.setEnabled(true); + userRep.setEmailVerified(false); + newuser = this.session != null && this.session.getContext() != null + ? RepresentationToModel.createUser(this.session, this.session.getContext().getRealm(), userRep) + : null; + if (newuser != null) { + return this.formattedReponse(Status.SUCCESS, json.getString("message")); + } else { + throw new Exception("Could not create the user"); } - }else { + + } else { throw new Exception("Publickey or emailId is null"); - } - }else{ + } + } else { throw new Exception("Could not parse the body"); } - }else { + } else { throw new Exception("Request body is null"); } - }catch(Exception e){ - return this.formattedReponse(Status.FAIL,e.toString()); - } + } catch (Exception e) { + return this.formattedReponse(Status.FAIL, e.toString()); + } } @POST @@ -211,41 +209,44 @@ public Response sign(String body) { String companyId = ""; String rawMessage = ""; HSUserModel user = null; - try{ - if(!body.isEmpty()){ + try { + if (!body.isEmpty()) { bodyObj = new JSONObject(body); - if(bodyObj != null){ - publickey = bodyObj.getString("publicKey"); sessionId = bodyObj.getString("ksSessionId"); - challange = bodyObj.getString("challenge"); signature = bodyObj.getString("signedRsv"); - companyId = bodyObj.getString("companyId"); rawMessage = bodyObj.getString("rawMsg"); - if(!publickey.isEmpty() || - !sessionId.isEmpty() || - !signature.isEmpty() || - !challange.isEmpty() || - !companyId.isEmpty()) { - if(isSignatureValid(body)){ - if (userSessionMap.containsKey(sessionId)){ - user = new HSUserModel(challange, publickey, true); - userSessionMap.put(sessionId, user); - return this.formattedReponse(Status.SUCCESS, "User authenticated"); - }else{ - throw new Exception("Invalid session"); - } - }else{ - throw new Exception("Invalid signature"); - } - }else { + if (bodyObj != null) { + publickey = bodyObj.getString("publicKey"); + sessionId = bodyObj.getString("ksSessionId"); + challange = bodyObj.getString("challenge"); + signature = bodyObj.getString("signedRsv"); + companyId = bodyObj.getString("companyId"); + rawMessage = bodyObj.getString("rawMsg"); + if (!publickey.isEmpty() || + !sessionId.isEmpty() || + !signature.isEmpty() || + !challange.isEmpty() || + !companyId.isEmpty()) { + if (isSignatureValid(body)) { + if (userSessionMap.containsKey(sessionId)) { + user = new HSUserModel(challange, publickey, true); + userSessionMap.put(sessionId, user); + return this.formattedReponse(Status.SUCCESS, "User authenticated"); + } else { + throw new Exception("Invalid session"); + } + } else { + throw new Exception("Invalid signature"); + } + } else { throw new Exception("Publickey, sessionId or signature is null"); - } - }else{ + } + } else { throw new Exception("Could not parse the body"); } - }else { + } else { throw new Exception("Request body is null"); } - }catch(Exception e){ - return this.formattedReponse(Status.FAIL,e.toString()); - } + } catch (Exception e) { + return this.formattedReponse(Status.FAIL, e.toString()); + } } @POST @@ -254,37 +255,37 @@ public Response sign(String body) { public String getNewSession(String body) { logger.info("session api called!"); JSONObject json = null; - try{ - if(!body.isEmpty()){ - if(userSessionMap != null){ + try { + if (!body.isEmpty()) { + if (userSessionMap != null) { json = new JSONObject(body); String ksSessionId = json.getString("kcSessionId"); String companyId = json.getString("companyId"); - if(json != null && !ksSessionId.isEmpty()){ + if (json != null && !ksSessionId.isEmpty()) { // call auth-server for session/challenge String url = this.getHsEP() + "challenge"; - String reqstBody = "{\"kcSessionId\" : \""+ksSessionId+"\", \"companyId\":\""+companyId+"\"}"; + String reqstBody = "{\"kcSessionId\" : \"" + ksSessionId + "\", \"companyId\":\"" + companyId + "\"}"; String response = AuthServerCaller.postApiCall(url, reqstBody); json = new JSONObject(response); - if(json != null && json.getInt("status") == 1){ + if (json != null && json.getInt("status") == 1) { // json = new JSONObject(json.get("data")); userSessionMap.put(ksSessionId, null); // this is kc session return json.getString("data"); //this.formattedReponse(Status.SUCCESS, sessionId); - }else{ + } else { throw new Exception(json.getString("message")); } - }else{ + } else { throw new Exception("Keycloak sessionid can not be null"); } - - }else{ + + } else { throw new Exception("userSessionMap is null"); } - }else{ + } else { throw new Exception("Request body can not be null"); } - - }catch(Exception e){ + + } catch (Exception e) { return e.toString();//this.formattedReponse(Status.FAIL,e.toString()); } } @@ -294,92 +295,91 @@ public String getNewSession(String body) { @Produces(MediaType.APPLICATION_JSON) public Response listenSuccess(@PathParam("sessionId") String sessionId) { logger.info("listen/success api called!"); - try{ - if(userSessionMap != null){ - if (userSessionMap.containsKey(sessionId) && userSessionMap.get(sessionId) != null){ + try { + if (userSessionMap != null) { + if (userSessionMap.containsKey(sessionId) && userSessionMap.get(sessionId) != null) { HSUserModel user = userSessionMap.get(sessionId); - if (user != null && user.hasLoggedIn){ - return this.formattedReponse(Status.SUCCESS, user.userId); - }else{ - throw new Exception("User not found or not validated"); + if (user != null && user.hasLoggedIn) { + return this.formattedReponse(Status.SUCCESS, user.userId); + } else { + throw new Exception("User not found or not validated"); } - }else{ - throw new Exception("Invalid session or user has not yet validated"); + } else { + throw new Exception("Invalid session or user has not yet validated"); } - }else{ + } else { throw new Exception("userSessionMap is null"); } - }catch(Exception e){ - return this.formattedReponse(Status.FAIL,e.toString()); - } + } catch (Exception e) { + return this.formattedReponse(Status.FAIL, e.toString()); + } } @GET @Path("listen/fail/{sessionId}") @Produces(MediaType.APPLICATION_JSON) - public Response listenFail(@PathParam("sessionId") String sessionId) { + public Response listenFail(@PathParam("sessionId") String sessionId) { logger.info("listen/fail api called!"); - try{ - if(userSessionMap != null){ - if (userSessionMap.containsKey(sessionId)){ + try { + if (userSessionMap != null) { + if (userSessionMap.containsKey(sessionId)) { userSessionMap.remove(sessionId); - return this.formattedReponse(Status.SUCCESS, "Session deleted"); - }else{ - throw new Exception("Invalid session"); + return this.formattedReponse(Status.SUCCESS, "Session deleted"); + } else { + throw new Exception("Invalid session"); } - }else{ + } else { throw new Exception("userSessionMap is null"); } - }catch(Exception e){ - return this.formattedReponse(Status.FAIL,e.toString()); - } + } catch (Exception e) { + return this.formattedReponse(Status.FAIL, e.toString()); + } } - private Response formattedReponse(Status status, String data){ + private Response formattedReponse(Status status, String data) { String respStr = ""; - try{ - FResponse response = new FResponse(); + try { + FResponse response = new FResponse(); response.status = status.name(); response.data = data; ObjectMapper Obj = new ObjectMapper(); // JSONObject bodyObj = new JSONObject(response); respStr = Obj.writeValueAsString(response); return Response - .status(Response.Status.OK) - .header("Access-Control-Allow-Origin", "*") - .header("Access-Control-Allow-Credentials", "true") - .header("Access-Control-Allow-Headers", "origin, content-type, accept, authorization") - .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD") - .entity(respStr) - .build(); - } - catch(Exception e){ + .status(Response.Status.OK) + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Credentials", "true") + .header("Access-Control-Allow-Headers", "origin, content-type, accept, authorization") + .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD") + .entity(respStr) + .build(); + } catch (Exception e) { respStr = e.toString(); - return Response - .status(Response.Status.BAD_REQUEST) - .header("Access-Control-Allow-Origin", "*") - .header("Access-Control-Allow-Headers", "origin, content-type, accept, authorization") - .header("Access-Control-Allow-Credentials", "true") - .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD") - .header("Access-Control-Max-Age", "1209600") - .entity(respStr) - .build(); - } + return Response + .status(Response.Status.BAD_REQUEST) + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Headers", "origin, content-type, accept, authorization") + .header("Access-Control-Allow-Credentials", "true") + .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD") + .header("Access-Control-Max-Age", "1209600") + .entity(respStr) + .build(); + } // return respStr; } - - private Boolean isSignatureValid(String body){ - try{ + + private Boolean isSignatureValid(String body) { + try { // call auth-server to validate this user. String url = this.getHsEP() + "verify"; - String responseFromAuthServer = AuthServerCaller.postApiCall(url,body); + String responseFromAuthServer = AuthServerCaller.postApiCall(url, body); JSONObject json = new JSONObject(responseFromAuthServer); - if(json != null && json.getInt("status") == 1){ + if (json != null && json.getInt("status") == 1) { return true; - }else{ + } else { return false; } - }catch(Exception e){ + } catch (Exception e) { return false; } } diff --git a/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/api/HSResourceProviderTest.java b/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/api/HSResourceProviderTest.java new file mode 100644 index 0000000..60f5d44 --- /dev/null +++ b/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/api/HSResourceProviderTest.java @@ -0,0 +1,36 @@ +package io.hypermine.hypersign.api; + + +import org.junit.runner.RunWith; +import org.keycloak.models.KeycloakSession; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import static org.junit.Assert.assertEquals; + +@RunWith(MockitoJUnitRunner.class) +public class HSResourceProviderTest { + @Mock + private KeycloakSession keycloakSession; + + + @org.junit.Test + public void register() { + String body = "{\n" + + " \"did\": \"12321321323d\",\n" + + " \"email\": \"dev@hypermine.com\",\n" + + " \"username\": \"dev gurung\",\n" + + " \"companyid\": \"hypermine\"\n" + + "}"; + HSResourceProvider hsResourceProvider = new HSResourceProvider(keycloakSession); + assertEquals("{\"status\":\"FAIL\",\"data\":\"java.lang.Exception: Could not create the user\"}", hsResourceProvider.register(body).getEntity()); + } + + @org.junit.Test + public void sign() { + } +} \ No newline at end of file From 23934f04744172d1036d7700a2cccd8a9456fffb Mon Sep 17 00:00:00 2001 From: Dev Gurung Date: Sun, 6 Dec 2020 20:26:34 +0545 Subject: [PATCH 2/3] Removing duplicate Junit entries --- hs-plugin-keycloak-ejb/pom.xml | 42 ---------------------------------- 1 file changed, 42 deletions(-) diff --git a/hs-plugin-keycloak-ejb/pom.xml b/hs-plugin-keycloak-ejb/pom.xml index 06f7c0d..6b014bb 100755 --- a/hs-plugin-keycloak-ejb/pom.xml +++ b/hs-plugin-keycloak-ejb/pom.xml @@ -119,48 +119,6 @@ 4.12 test - - junit - junit - 4.12 - test - - - junit - junit - 4.12 - test - - - junit - junit - 4.12 - test - - - junit - junit - 4.12 - test - - - junit - junit - 4.12 - test - - - junit - junit - 4.12 - test - - - junit - junit - 4.12 - test - From 1c76fe91dbc19af8ea387216955cc166e2cc6008 Mon Sep 17 00:00:00 2001 From: Dev Gurung Date: Sun, 20 Dec 2020 12:02:06 +0545 Subject: [PATCH 3/3] HS-ADAPTER:FEARURE:JWT token generation with GUID as payload --- hs-plugin-keycloak-ejb/pom.xml | 6 + .../authenticator/HyperSignAuthenticator.java | 130 ++++--- .../hypermine/hypersign/jwt/JWTManager.java | 44 +++ .../HyperSignAuthenticatorTest.java | 32 ++ .../authenticator/MockLoginFormProvider.java | 325 ++++++++++++++++++ .../hypersign/jwt/JWTManagerTest.java | 33 ++ 6 files changed, 498 insertions(+), 72 deletions(-) create mode 100644 hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/jwt/JWTManager.java create mode 100644 hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/authenticator/HyperSignAuthenticatorTest.java create mode 100644 hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/authenticator/MockLoginFormProvider.java create mode 100644 hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/jwt/JWTManagerTest.java diff --git a/hs-plugin-keycloak-ejb/pom.xml b/hs-plugin-keycloak-ejb/pom.xml index 6b014bb..b747a46 100755 --- a/hs-plugin-keycloak-ejb/pom.xml +++ b/hs-plugin-keycloak-ejb/pom.xml @@ -119,6 +119,12 @@ 4.12 test + + io.jsonwebtoken + jjwt + 0.9.1 + + diff --git a/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/authenticator/HyperSignAuthenticator.java b/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/authenticator/HyperSignAuthenticator.java index 356c481..1b04d44 100644 --- a/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/authenticator/HyperSignAuthenticator.java +++ b/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/authenticator/HyperSignAuthenticator.java @@ -17,6 +17,8 @@ package io.hypermine.hypersign.authenticator; +import io.hypermine.hypersign.jwt.JWTManager; +import org.hibernate.id.GUIDGenerator; import org.jboss.resteasy.spi.HttpResponse; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.authentication.AuthenticationFlowContext; @@ -35,6 +37,8 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import java.net.URI; +import java.util.UUID; + import org.json.JSONObject; import io.hypermine.hypersign.service.AuthServerCaller; @@ -48,7 +52,7 @@ public class HyperSignAuthenticator implements Authenticator { public static final String CREDENTIAL_TYPE = "hypersign_qrcode"; private static ServicesLogger logger = ServicesLogger.LOGGER; - + protected boolean hasCookie(AuthenticationFlowContext context) { Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("HYPERSIGN_QRCODE_SOLVED"); boolean result = cookie != null; @@ -58,7 +62,7 @@ protected boolean hasCookie(AuthenticationFlowContext context) { /********************************************************************************** * This will check if browser has already tried solving the challenge before, if that * is the case then it wont again give the same challenge, rather it will bypass it - * + * * ********************************************************************************/ @Override public void authenticate(AuthenticationFlowContext context) { @@ -73,38 +77,20 @@ public void authenticate(AuthenticationFlowContext context) { logger.info("HyperSignAuthenticator :: authenticate : ends"); } - protected Response getChallenge(AuthenticationFlowContext context){ + protected Response getChallenge(AuthenticationFlowContext context) { + int jwtTimeToLive = 0; + UUID uuid = UUID.randomUUID(); logger.info("HyperSignAuthenticator :: getChallenge : starts"); - String url = getFormattedUrl(context, "session"); // baseUrl + "/auth/realms/"+ relam +"/hypersign/session"; - // blocking call to get new session - System.out.println("************************"); - System.out.println(context.getUriInfo().getQueryParameters().getFirst("state")); - String executionId = context.getUriInfo().getQueryParameters().getFirst("state"); - String newHSsession = ""; - String relamName = context.getRealm().getName(); - Response challenge = null; - if(executionId != null && !executionId.isEmpty()){ - String body = "{\"kcSessionId\":\""+executionId+"\", \"companyId\" : \""+relamName+"\"}"; - newHSsession = callAPi(url, body); - System.out.println("************************"); - System.out.println(newHSsession); - logger.info("HyperSignAuthenticator :: getChallenge : after"); - String qrJson = "{\"kcSessionId\":\""+executionId+"\", \"companyId\" : \""+relamName+"\", \"hsSessionId\" : \""+newHSsession+"\"}"; - String hsQr = QRCodeGenerator.createORLoginPage(qrJson); - challenge = context.form() - .setAttribute("loginMethod", "UAF") - .setAttribute("hsSession",newHSsession) - .setAttribute("ksSessionId",executionId) - .setAttribute("hsQr",hsQr) - .createForm("hypersign-new.ftl"); - } - return challenge; + String jwtToken = JWTManager.createJWT(jwtTimeToLive, uuid.toString()); + String hsQr = QRCodeGenerator.createORLoginPage(jwtToken); + return context.form().setAttribute("hsQr", hsQr).createForm("hypersign-new.ftl"); + } /********************************************************************************** * This is the main method that will get called once user solves the challenge and * click on the submit button. - * + * * ********************************************************************************/ @Override public void action(AuthenticationFlowContext context) { @@ -125,72 +111,72 @@ public void action(AuthenticationFlowContext context) { logger.info("HyperSignAuthenticator :: action : ends"); } - private String callAPi(String url, String body){ - try{ + private String callAPi(String url, String body) { + try { logger.info("HyperSignAuthenticator :: callApi : start"); logger.info("HyperSignAuthenticator :: callApi : url :"); logger.info(url); - if(body != null && !body.isEmpty()){ + if (body != null && !body.isEmpty()) { return AuthServerCaller.postApiCall(url, body); - }else{ + } else { return AuthServerCaller.getApiCall(url); } - }catch(Exception e){ + } catch (Exception e) { logger.error(e); return ""; } } - + private String getFormattedUrl(AuthenticationFlowContext context, String endpoint) { - String baseUrl = ""; - String relam = ""; - String url=""; - if(context != null) { - baseUrl = (context.getUriInfo() !=null && context.getUriInfo().getBaseUri() != null) - ? context.getUriInfo().getBaseUri().toString() - : "http://localhost:8080/auth/"; - relam = (context.getRealm() != null && context.getRealm().getName() != null && !context.getRealm().getName().isEmpty()) - ? context.getRealm().getName() - : "master"; - url = baseUrl + "realms/"+ relam +"/hypersign/" + endpoint; - } - return url; - } + String baseUrl = ""; + String relam = ""; + String url = ""; + if (context != null) { + baseUrl = (context.getUriInfo() != null && context.getUriInfo().getBaseUri() != null) + ? context.getUriInfo().getBaseUri().toString() + : "http://localhost:8080/auth/"; + relam = (context.getRealm() != null && context.getRealm().getName() != null && !context.getRealm().getName().isEmpty()) + ? context.getRealm().getName() + : "master"; + url = baseUrl + "realms/" + relam + "/hypersign/" + endpoint; + } + return url; + } private boolean validateUser(AuthenticationFlowContext context) { - Boolean isValid = false; + Boolean isValid = false; String url = ""; MultivaluedMap formData = null; String userIdFromForm = ""; String sessionId = ""; try { - logger.info("HyperSignAuthenticator :: validateUser : starts "); + logger.info("HyperSignAuthenticator :: validateUser : starts "); formData = context.getHttpRequest().getDecodedFormParameters(); - + sessionId = formData.getFirst("ksSessionId"); logger.info("HyperSignAuthenticator :: validateUser : sessionId :"); logger.info(sessionId); - + userIdFromForm = formData.getFirst("userId"); logger.info("HyperSignAuthenticator :: validateUser : userId in form :"); logger.info(userIdFromForm); - + url = getFormattedUrl(context, "listen/success/" + sessionId); - + // check if this user is correct from api call; // blocking api call - String resp = callAPi(url,""); - JSONObject json = new JSONObject(resp); + String resp = callAPi(url, ""); + JSONObject json = new JSONObject(resp); String userIdFromAPi = ""; - if(json != null){ + if (json != null) { userIdFromAPi = json.getString("data"); logger.info("HyperSignAuthenticator :: validateUser : userId in api :"); logger.info(userIdFromAPi); - }else{ + } else { logger.info("User authentication failed from hs-auth-server"); } - - if(userIdFromAPi !=null && !userIdFromAPi.isEmpty() && userIdFromAPi.equals(userIdFromForm)){ + + if (userIdFromAPi != null && !userIdFromAPi.isEmpty() && userIdFromAPi.equals(userIdFromForm)) { logger.info("HyperSignAuthenticator :: validateUser : User is valid"); UserCredentialModel input = new UserCredentialModel(); // input.setType(SecretQuestionCredentialProvider.QR_CODE); @@ -202,27 +188,27 @@ private boolean validateUser(AuthenticationFlowContext context) { setCookie(context); context.success(); isValid = true; - }else{ + } else { logger.info("HyperSignAuthenticator :: validateUser : User is not valid"); logger.info("HyperSignAuthenticator :: validateUser : Clear session of this user"); // clear session in case of failure url = getFormattedUrl(context, "listen/fail/" + sessionId); - callAPi(url,""); + callAPi(url, ""); context.cancelLogin(); - } - }catch (Exception e) { + } + } catch (Exception e) { logger.error(e.toString()); - // TODO: handle exception - } + // TODO: handle exception + } logger.info("HyperSignAuthenticator :: validateUser : ends "); return isValid; } - + /********************************************************************************** * Set the Hypersign cookie for 30 days. - * + * * ********************************************************************************/ - + protected void setCookie(AuthenticationFlowContext context) { AuthenticatorConfigModel config = context.getAuthenticatorConfig(); int maxCookieAge = 60 * 60 * 24 * 30; // 30 days @@ -248,7 +234,7 @@ public static void addCookie(String name, String value, String path, String doma /********************************************************************************** * We are setting this as false since we dont need the user information as of now. - * + * * ********************************************************************************/ @Override public boolean requiresUser() { @@ -257,12 +243,12 @@ public boolean requiresUser() { /********************************************************************************** * Setting this as false sine we do not want to store any information - * + * * ********************************************************************************/ @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { //return false; - return session.userCredentialManager().isConfiguredFor(realm, user, HyperSignCredentialProvider.QR_CODE); + return session.userCredentialManager().isConfiguredFor(realm, user, HyperSignCredentialProvider.QR_CODE); } @Override diff --git a/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/jwt/JWTManager.java b/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/jwt/JWTManager.java new file mode 100644 index 0000000..b6fd283 --- /dev/null +++ b/hs-plugin-keycloak-ejb/src/main/java/io/hypermine/hypersign/jwt/JWTManager.java @@ -0,0 +1,44 @@ +package io.hypermine.hypersign.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import javax.crypto.spec.SecretKeySpec; +import javax.xml.bind.DatatypeConverter; +import java.security.Key; +import java.util.Date; + +public class JWTManager { + + private static String SECRET_KEY = "oeRaYY7Wo24sDqKSX3IM9ASGmdGPmkTd9jo1QTy4b7P9Ze5_9hKolVX8xNrQDcNRfVEdTZNOuOyqEGhXEbdJI-ZQ19k_o9MI0y3eZN2lp9jow55FfXMiINEdt1XR85VipRLSOkT6kSpzs2x-jbLDiz9iFVzkd81YKxMgPA7VfZeQUm4n-mOmnWMaVX30zGFU4L3oPBctYKkl4dYfqYWqRNfrgPJVi5DGFjywgxx0ASEiJHtV72paI3fDR2XwlSkyhhmY-ICjCRmsJN4fX1pdoL8a18-aQrvyu4j0Os6dVPYIoPvvY0SAZtWYKHfM15g7A3HD4cVREf9cUsprCRK93w"; + + public static String createJWT(long ttlMillis , String payLoad) { + + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + + long nowMillis = System.currentTimeMillis(); + Date now = new Date(nowMillis); + byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY); + Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); + JwtBuilder builder = Jwts.builder().claim("GUID" ,payLoad) + .signWith(signatureAlgorithm, signingKey); + + if (ttlMillis >= 0) { + long expMillis = nowMillis + ttlMillis; + Date exp = new Date(expMillis); + builder.setExpiration(exp); + } + + return builder.compact(); + } + + public static Claims decodeJWT(String jwt) { + + Claims claims = Jwts.parser() + .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY)) + .parseClaimsJws(jwt).getBody(); + return claims; + } +} diff --git a/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/authenticator/HyperSignAuthenticatorTest.java b/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/authenticator/HyperSignAuthenticatorTest.java new file mode 100644 index 0000000..8e86d1b --- /dev/null +++ b/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/authenticator/HyperSignAuthenticatorTest.java @@ -0,0 +1,32 @@ +package io.hypermine.hypersign.authenticator; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProvider; +import org.keycloak.models.KeycloakSession; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +public class HyperSignAuthenticatorTest { + + @Mock + private AuthenticationFlowContext authenticationFlowContext; + @Test + public void getChallenge() { + PowerMockito.mock(AuthenticationFlowContext.class); + when(authenticationFlowContext.form()).thenReturn(new MockLoginFormProvider()); + HyperSignAuthenticator hyperSignAuthenticator = new HyperSignAuthenticator(); + assertNotNull (hyperSignAuthenticator.getChallenge(authenticationFlowContext)); + + } +} \ No newline at end of file diff --git a/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/authenticator/MockLoginFormProvider.java b/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/authenticator/MockLoginFormProvider.java new file mode 100644 index 0000000..4494bd6 --- /dev/null +++ b/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/authenticator/MockLoginFormProvider.java @@ -0,0 +1,325 @@ +package io.hypermine.hypersign.authenticator; + +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.FormMessage; +import org.keycloak.sessions.AuthenticationSessionModel; + +import javax.ws.rs.core.*; +import java.lang.annotation.Annotation; +import java.net.URI; +import java.util.*; + +/*This is to be used while mocking the response from the getChallenge method*/ +public class MockLoginFormProvider implements LoginFormsProvider { + @Override + public void addScript(String scriptUrl) { + + } + + @Override + public Response createResponse(UserModel.RequiredAction action) { + return null; + } + + @Override + public Response createForm(String form) { + return new Response() { + @Override + public int getStatus() { + return 0; + } + + @Override + public StatusType getStatusInfo() { + return null; + } + + @Override + public Object getEntity() { + return null; + } + + @Override + public T readEntity(Class entityType) { + return null; + } + + @Override + public T readEntity(GenericType entityType) { + return null; + } + + @Override + public T readEntity(Class entityType, Annotation[] annotations) { + return null; + } + + @Override + public T readEntity(GenericType entityType, Annotation[] annotations) { + return null; + } + + @Override + public boolean hasEntity() { + return false; + } + + @Override + public boolean bufferEntity() { + return false; + } + + @Override + public void close() { + + } + + @Override + public MediaType getMediaType() { + return null; + } + + @Override + public Locale getLanguage() { + return null; + } + + @Override + public int getLength() { + return 0; + } + + @Override + public Set getAllowedMethods() { + return null; + } + + @Override + public Map getCookies() { + return null; + } + + @Override + public EntityTag getEntityTag() { + return null; + } + + @Override + public Date getDate() { + return null; + } + + @Override + public Date getLastModified() { + return null; + } + + @Override + public URI getLocation() { + return null; + } + + @Override + public Set getLinks() { + return null; + } + + @Override + public boolean hasLink(String relation) { + return false; + } + + @Override + public Link getLink(String relation) { + return null; + } + + @Override + public Link.Builder getLinkBuilder(String relation) { + return null; + } + + @Override + public MultivaluedMap getMetadata() { + return null; + } + + @Override + public MultivaluedMap getStringHeaders() { + return null; + } + + @Override + public String getHeaderString(String name) { + return null; + } + }; + } + + @Override + public String getMessage(String message) { + return null; + } + + @Override + public String getMessage(String message, String... parameters) { + return null; + } + + @Override + public Response createLogin() { + return null; + } + + @Override + public Response createPasswordReset() { + return null; + } + + @Override + public Response createLoginTotp() { + return null; + } + + @Override + public Response createRegistration() { + return null; + } + + @Override + public Response createInfoPage() { + return null; + } + + @Override + public Response createUpdateProfilePage() { + return null; + } + + @Override + public Response createIdpLinkConfirmLinkPage() { + return null; + } + + @Override + public Response createIdpLinkEmailPage() { + return null; + } + + @Override + public Response createLoginExpiredPage() { + return null; + } + + @Override + public Response createErrorPage(Response.Status status) { + return null; + } + + @Override + public Response createOAuthGrant() { + return null; + } + + @Override + public Response createCode() { + return null; + } + + @Override + public Response createX509ConfirmPage() { + return null; + } + + @Override + public LoginFormsProvider setAuthenticationSession(AuthenticationSessionModel authenticationSession) { + return null; + } + + @Override + public LoginFormsProvider setClientSessionCode(String accessCode) { + return null; + } + + @Override + public LoginFormsProvider setAccessRequest(List clientScopesRequested) { + return null; + } + + @Override + public LoginFormsProvider setError(String message, Object... parameters) { + return null; + } + + @Override + public LoginFormsProvider setErrors(List messages) { + return null; + } + + @Override + public LoginFormsProvider addError(FormMessage errorMessage) { + return null; + } + + @Override + public LoginFormsProvider addSuccess(FormMessage errorMessage) { + return null; + } + + @Override + public LoginFormsProvider setSuccess(String message, Object... parameters) { + return null; + } + + @Override + public LoginFormsProvider setInfo(String message, Object... parameters) { + return null; + } + + @Override + public LoginFormsProvider setUser(UserModel user) { + return null; + } + + @Override + public LoginFormsProvider setResponseHeader(String headerName, String headerValue) { + return null; + } + + @Override + public LoginFormsProvider setFormData(MultivaluedMap formData) { + return null; + } + + @Override + public LoginFormsProvider setAttribute(String name, Object value) { + return new MockLoginFormProvider(); + } + + @Override + public LoginFormsProvider setStatus(Response.Status status) { + return null; + } + + @Override + public LoginFormsProvider setMediaType(MediaType type) { + return null; + } + + @Override + public LoginFormsProvider setActionUri(URI requestUri) { + return null; + } + + @Override + public LoginFormsProvider setExecution(String execution) { + return null; + } + + @Override + public void close() { + + } +} diff --git a/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/jwt/JWTManagerTest.java b/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/jwt/JWTManagerTest.java new file mode 100644 index 0000000..f308a0b --- /dev/null +++ b/hs-plugin-keycloak-ejb/src/test/java/io/hypermine/hypersign/jwt/JWTManagerTest.java @@ -0,0 +1,33 @@ +package io.hypermine.hypersign.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.MalformedJwtException; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class JWTManagerTest { + + @Test + public void createJWT() { + String payLoad = "1234-3434-34343"; + int jwtTimeToLive = 800000; + + String jwt = JWTManager.createJWT( + jwtTimeToLive, + payLoad + ); + Claims claims = JWTManager.decodeJWT(jwt); + assertEquals("1234-3434-34343", claims.get("GUID")); + } + + + @Test(expected = MalformedJwtException.class) + public void decodeShouldFail() { + + String notAJwt = "This is not a JWT"; + + Claims claims = JWTManager.decodeJWT(notAJwt); + + } +} \ No newline at end of file