diff --git a/README.md b/README.md index 7722d3a..b94a976 100644 --- a/README.md +++ b/README.md @@ -17,32 +17,32 @@ The project contains one flow: `org.gluu.agama.typekey`. When this is launched, 1. A running instance of Jans Auth Server 1. A new column in `jansdb.jansPerson` to store the phrase metadata in -1. A SCAN subscription. Please visit [https://gluu.org/agama-lab] and sign up for a free SCAN subscription, which gives you 500 credits. Each successful Typekey API call costs 25 credits. +1. A SCAN subscription. Please visit [Agama Lab](https://gluu.org/agama-lab) and sign up for a free SCAN subscription, which gives you 500 credits. Each successful Typekey API call costs 4 credits. ### Add column to database -These instructions are for MySQL. Please follow the [documentation](https://docs.jans.io/v1.0.22/admin/reference/database/) for your persistence type. +These instructions are for PostgreSQL. Please follow the [documentation](https://docs.jans.io/v1.0.22/admin/reference/database/) for your persistence type. 1. Log into the server running Jans -2. Log into MySQL with a user that has permission to operate on `jansdb` -3. Add the column: +2. Log into PostgreSQL with a user that has permission to operate on `jansdb` +3. Connect to `jansdb`: `\c jansdb` +4. Add the column: ```sql - ALTER TABLE jansdb.jansPerson ADD COLUMN typekeyData JSON NULL; + ALTER TABLE "jansPerson" ADD COLUMN typekeyData JSON; ``` -4. Restart MySQL and Auth Server to load the changes: +4. Restart PostgreSQL and Auth Server to load the changes: ``` - systemctl restart mysql jans-auth + systemctl restart postgresql jans-auth ```` ### Dynamic Client Registration -In order to call the Typekey API, you will need an OAuth client. Once you have a SCAN subscription on Agama Lab, navigate to `Market` > `SCAN` and create an SSA with the software claim `typekey` and an appropriate lifetime. Your client will expire after that time. Once this is done, note down the base64 encoded string, and send a dynamic client registration request to `https://account.gluu.org/jans-auth/restv1/register` to obtain a client ID and secret. You will need this to configure the Typekey flow. Jans Tarp has functionality to automate the registration process. +In order to call the Typekey API, you will need an OAuth client. Once you have a SCAN subscription on Agama Lab, navigate to `Market` > `SCAN` and create an SSA with the software claim `typekey`. The Typekey flow will register its own client via DCR with the SSA you provide in the configuration. - [Dynamic Client Registration specification](https://www.rfc-editor.org/rfc/rfc7591#section-3.1) -- [Jans Tarp](https://github.com/JanssenProject/jans/tree/main/demos/jans-tarp) ### Deployment @@ -69,11 +69,14 @@ Follow the steps below: "org.gluu.agama.typekey": { "keystoreName": "", "keystorePassword": "", - "orgId": "", - "clientId": "", - "clientSecret": "", + "orgId": "", + "scan_ssa": "", "authHost": "https://account.gluu.org", - "scanHost": "https://cloud.gluu.org" + "scanHost": "https://cloud.gluu.org", + "phrases": { + "1": "itwasthebestoftimes", + "2": "itwastheworstoftimes" + } } } ``` @@ -82,8 +85,9 @@ Follow the steps below: - `keystoreName` and `keystorePassword` are optional, in case you want to include a signature when sending the Typekey data. Leave them as blank otherwise. - `orgId` is the organization ID that can be obtained by decoding the software statement JWT and looking at the `org_id` claim (You may use `https://jwt.io` to decode the SSA). -- `clientId` and `clientSecret` are the client credentials obtained from Dynamic Client Registration +- `scan_ssa` is the JWT string you obtain from Agama Lab - `authHost` and `scanHost` can be left as is +- `phrases` is explained in the [Details](#details) section - We go back to the TUI and click on `Import Configuration` and select the modified configuration file with our parameters. - With this, our `agama project` is now configured and we can start testing. @@ -96,7 +100,35 @@ or [jans-tent](https://github.com/JanssenProject/jans/tree/main/demos/jans-tent) Launch an authorization flow with parameters `acr_values=agama&agama_flow=org.gluu.agama.typekey` with your chosen RP. -Check out this video to see an example of **agama-typekey** in action: +## Details +The first time a user starts the Typekey flow, Typekey will choose a random phrase from the `phrases` dict in the configuration and store it in persistence. Then, the Typekey API is called to provide the keystroke data recorded during the flow. The first 5 times, Typekey API will train on the data provided. This phase is called "Enrollment". On the 6th attempt onward, Typekey API will validate the provided keystroke data using the training data stored during enrollment. If the behavioral data is sufficiently different from the trained data, Typekey API will deny the request. +In case Typekey API denies the request, Agama Typekey falls back to password authentication, and retrains the API on the provided data. + +## Examples + +Enrollment: + + +https://github.com/SafinWasi/agama-typekey/assets/6601566/2256877b-3b49-48d8-b292-3d9da4a3a4c5 + + + +Typekey API approval: + + + +https://github.com/SafinWasi/agama-typekey/assets/6601566/de5dcb19-9fbb-41f3-b897-606fc52fce85 + + + + +Typekey API denied, fallback to password: + + + +https://github.com/SafinWasi/agama-typekey/assets/6601566/b0288f5c-6a84-4ea0-b6a4-ac9052409189 + + # Contributors diff --git a/code/org.gluu.agama.typekey.flow b/code/org.gluu.agama.typekey.flow index 4a1838a..8d7d528 100644 --- a/code/org.gluu.agama.typekey.flow +++ b/code/org.gluu.agama.typekey.flow @@ -1,45 +1,49 @@ Flow org.gluu.agama.typekey - Basepath "" - Configs conf + Basepath "" + Configs conf idp = Call org.gluu.agama.typekey.IdentityProcessor#new tk = Call org.gluu.agama.typekey.Typekey#new conf user = RRF "typekey/username.ftlh" userData = Call idp accountFromUsername user.username When userData.empty is true - it_vsrve = {success:false, error: "User not found"} - Finish it_vsrve + it_vsrve = {success:false, error: "User not found"} + Finish it_vsrve +Call tk dynamicRegistration conf.scan_ssa enrolled = Call idp enrolled user.username When enrolled is false - random_usecase = Call tk getRandomUseCase - phrase_map = Call tk generateTypekeyData random_usecase - dummy = Call idp enroll user.username phrase_map - phrase = phrase_map.phrase - use_case = random_usecase + random_usecase = Call tk getRandomUseCase + phrase_map = Call tk generateTypekeyData random_usecase + dummy = Call idp enroll user.username phrase_map + phrase = phrase_map.phrase + use_case = random_usecase When enrolled is true - typekey_data = Call idp getTypekeyData user.username - phrase = typekey_data.phrase - use_case = typekey_data.useCase + typekey_data = Call idp getTypekeyData user.username + phrase = typekey_data.phrase + use_case = typekey_data.useCase phraseDict = {phrase:phrase} phraseData = RRF "typekey/phrase.ftlh" phraseDict typekey_result = Call tk validateKeystrokes user.username phraseData.phrase_data use_case When typekey_result.status is "Enrollment" - password = RRF "typekey/password.ftlh" - authenticated = Call idp authenticate user.username password.pwd - When authenticated is true - Call tk notifyKeystrokes user.username typekey_result.track_id use_case - it_spikk = {success:true, data: { userId: user.username}} - Finish it_spikk - it_ttqbc = {success:false, error: "Authentication failed"} - Finish it_ttqbc + Log "Agama Typekey: Enrollment in progress" + password = RRF "typekey/password.ftlh" + authenticated = Call idp authenticate user.username password.pwd + When authenticated is true + Call tk notifyKeystrokes user.username typekey_result.track_id use_case + it_spikk = {success:true, data: { userId: user.username}} + Finish it_spikk + it_ttqbc = {success:false, error: "Authentication failed"} + Finish it_ttqbc When typekey_result.status is "Approved" - it_zirls = {success:true, data: { userId: user.username}} - Finish it_zirls + Log "Agama Typekey: Approved" + it_zirls = {success:true, data: { userId: user.username}} + Finish it_zirls password = RRF "typekey/password.ftlh" authenticated = Call idp authenticate user.username password.pwd When authenticated is true - When typekey_result.status is "Denied" - Call tk notifyKeystrokes user.username typekey_result.track_id use_case - it_becry = {success:true, data: { userId: user.username }} - Finish it_becry + When typekey_result.status is "Denied" + Log "Denied, fell back to password" + Call tk notifyKeystrokes user.username typekey_result.track_id use_case + it_becry = {success:true, data: { userId: user.username }} + Finish it_becry it_ryekg = {success:false, error: "Typekey and password failed"} -Finish it_ryekg \ No newline at end of file +Finish it_ryekg diff --git a/lib/org/gluu/agama/typekey/Typekey.java b/lib/org/gluu/agama/typekey/Typekey.java index b3b7824..a1b974a 100644 --- a/lib/org/gluu/agama/typekey/Typekey.java +++ b/lib/org/gluu/agama/typekey/Typekey.java @@ -11,6 +11,7 @@ import io.jans.as.model.crypto.AuthCryptoProvider; import io.jans.service.cdi.util.CdiUtil; import io.jans.util.StringHelper; +import io.jans.service.CacheService; import java.net.URL; import java.net.URLEncoder; @@ -70,6 +71,32 @@ private String buildServiceAuth() throws Exception { return "Bearer " + r.getContentAsJSONObject().getAsString("access_token"); } + public void dynamicRegistration(String scanSSA) { + String asEndpoint = config.getAuthHost() + "/jans-auth/restv1/register"; + HTTPRequest request = new HTTPRequest(HTTPRequest.Method.POST, new URL(asEndpoint)); + request.setAccept(APPLICATION_JSON); + request.setContentType(APPLICATION_JSON); + request.setConnectTimeout(3000); + request.setReadTimeout(3000); + JSONArray redirect_array = new JSONArray(); + redirect_array.put(config.getAuthHost()); + JSONArray grant_array = new JSONArray(); + grant_array.put("client_credentials"); + Map map = new HashMap(Map.of( + "client_name", "typekey_client", + "redirect_uris", redirect_array, + "grant_types", grant_array, + "software_statement", scanSSA, + "lifetime", 86400)); + String message = new JSONObject(map).toString(); + request.setQuery(message); + HTTPResponse r = request.send(); + r.ensureStatusCode(201); + logger.info("Client registration successful"); + config.setClientId(r.getContentAsJSONObject().getAsString("client_id")); + config.setClientSecret(r.getContentAsJSONObject().getAsString("client_secret")); + } + private String signUid(String uid, String alias) throws Exception { AuthCryptoProvider auth = new AuthCryptoProvider(config.getKeystoreName(), config.getKeystorePassword(), null); String signedUid = auth.sign(uid, alias, null, SignatureAlgorithm.RS256); @@ -96,43 +123,21 @@ public Map validateKeystrokes(String username, String k_data, St request.setQuery(message); request.setAuthorization(token); HTTPResponse r = request.send(); - Map responseObject; - - if (r.getStatusCode() == 200) { - responseObject = r.getContentAsJSONObject(); - return responseObject; - } else { - int statusCode = r.getStatusCode(); - responseObject = new HashMap(); - switch (statusCode) { - case 401: - responseObject.put("status", "Unauthorized"); - break; - case 403: - responseObject.put("status", "Forbidden"); - break; - case 422: - responseObject.put("status", "Unprocessable entity"); - break; - case 400: - responseObject.put("status", "Bad request"); - break; - default: - responseObject.put("status", "Other error"); - logger.info("Other error. Status code: {}", statusCode); - break; - } - } + + r.ensureStatusCode(200); + Map responseObject = r.getContentAsJSONObject(); + return responseObject; } - public void notifyKeystrokes(String uid, int track_id) { + public void notifyKeystrokes(String uid, int track_id, String use_case) { + int useCase = Integer.parseInt(use_case); String token = buildServiceAuth(); Map map = new HashMap(Map.of( "uid", uid, "track_id", track_id, "org_id", config.getOrgId(), - "use_case", 1)); + "use_case", useCase)); String endpointUrl = config.getScanHost() + "/scan/typekey/notify"; String message = new JSONObject(map).toString(); logger.info(message); diff --git a/project.json b/project.json index ebf0b72..e98b3dd 100644 --- a/project.json +++ b/project.json @@ -17,8 +17,7 @@ "keystoreName": "", "keystorePassword": "", "orgId": "", - "clientId": "", - "clientSecret": "", + "scan_ssa": "", "authHost": "https://account.gluu.org", "scanHost": "https://cloud.gluu.org", "phrases": { @@ -28,4 +27,4 @@ } }, "name": "agama-typekey" -} \ No newline at end of file +} diff --git a/web/typekey/username.ftlh b/web/typekey/username.ftlh index 774a5a6..cfe5a63 100644 --- a/web/typekey/username.ftlh +++ b/web/typekey/username.ftlh @@ -1,18 +1,11 @@ -<#import "commons.ftlh" as com> - <@com.main> -
-
-
-
- - -
-
-
-
- -
-
-
-
- +[#ftl output_format="HTML"] + + + +[#import "commons.ftlh" as com] + [@com.main] +
+ [/@com.main] + + + \ No newline at end of file