Skip to content

Commit

Permalink
Merge pull request #5 from SafinWasi/agama-lab-branch
Browse files Browse the repository at this point in the history
Ok, let's see
  • Loading branch information
nynymike authored Mar 15, 2024
2 parents a6e20f4 + 7951861 commit d6f1481
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 92 deletions.
62 changes: 47 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
}
}
}
```
Expand All @@ -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.
Expand All @@ -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
Expand Down
58 changes: 31 additions & 27 deletions code/org.gluu.agama.typekey.flow
Original file line number Diff line number Diff line change
@@ -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
Finish it_ryekg
63 changes: 34 additions & 29 deletions lib/org/gluu/agama/typekey/Typekey.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object> 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);
Expand All @@ -96,43 +123,21 @@ public Map<String, Object> validateKeystrokes(String username, String k_data, St
request.setQuery(message);
request.setAuthorization(token);
HTTPResponse r = request.send();
Map<String, Object> responseObject;

if (r.getStatusCode() == 200) {
responseObject = r.getContentAsJSONObject();
return responseObject;
} else {
int statusCode = r.getStatusCode();
responseObject = new HashMap<String, Object>();
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<String, Object> 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<String, Object> 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);
Expand Down
5 changes: 2 additions & 3 deletions project.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
"keystoreName": "",
"keystorePassword": "",
"orgId": "",
"clientId": "",
"clientSecret": "",
"scan_ssa": "",
"authHost": "https://account.gluu.org",
"scanHost": "https://cloud.gluu.org",
"phrases": {
Expand All @@ -28,4 +27,4 @@
}
},
"name": "agama-typekey"
}
}
29 changes: 11 additions & 18 deletions web/typekey/username.ftlh
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
<#import "commons.ftlh" as com>
<@com.main>
<div class="border border-1 rounded p-5">
<form id="login_form" method="POST">
<div class="mb-3 row">
<div class="col-md-8">
<label for="username" class="col-md-4 col-form-label">Enter your username:</label>
<input type="text" class="form-control" id="username" name="username">
</div>
</div>
<div class="row">
<div class="col-md-12 d-flex justify-content-end">
<input type="submit" class="btn btn-success" id="login" value="Login">
</div>
</div>
</form>
</div>
</@com.main>
[#ftl output_format="HTML"]
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>[#import "commons.ftlh" as com]
[@com.main]
<div class="border border-1 rounded p-5"><form method="POST" id="login_form"><div class="mb-3 row"><div class="col-md-8"><label for="username" class="col-md-4 col-form-label">Enter your username:</label><input type="text" id="username" name="username" class="form-control"></div></div><div class="row"><div class="col-md-12 d-flex justify-content-end"><input type="submit" id="login" value="Login" class="btn btn-success"></div></div></form></div>
[/@com.main]</body>


</html>

0 comments on commit d6f1481

Please sign in to comment.