Skip to content

Commit d6f1481

Browse files
authored
Merge pull request #5 from SafinWasi/agama-lab-branch
Ok, let's see
2 parents a6e20f4 + 7951861 commit d6f1481

File tree

5 files changed

+125
-92
lines changed

5 files changed

+125
-92
lines changed

README.md

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,32 @@ The project contains one flow: `org.gluu.agama.typekey`. When this is launched,
1717

1818
1. A running instance of Jans Auth Server
1919
1. A new column in `jansdb.jansPerson` to store the phrase metadata in
20-
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.
20+
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.
2121

2222
### Add column to database
2323

24-
These instructions are for MySQL. Please follow the [documentation](https://docs.jans.io/v1.0.22/admin/reference/database/) for your persistence type.
24+
These instructions are for PostgreSQL. Please follow the [documentation](https://docs.jans.io/v1.0.22/admin/reference/database/) for your persistence type.
2525

2626
1. Log into the server running Jans
27-
2. Log into MySQL with a user that has permission to operate on `jansdb`
28-
3. Add the column:
27+
2. Log into PostgreSQL with a user that has permission to operate on `jansdb`
28+
3. Connect to `jansdb`: `\c jansdb`
29+
4. Add the column:
2930

3031
```sql
31-
ALTER TABLE jansdb.jansPerson ADD COLUMN typekeyData JSON NULL;
32+
ALTER TABLE "jansPerson" ADD COLUMN typekeyData JSON;
3233
```
3334

34-
4. Restart MySQL and Auth Server to load the changes:
35+
4. Restart PostgreSQL and Auth Server to load the changes:
3536

3637
```
37-
systemctl restart mysql jans-auth
38+
systemctl restart postgresql jans-auth
3839
````
3940
4041
### Dynamic Client Registration
4142
42-
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.
43+
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.
4344
4445
- [Dynamic Client Registration specification](https://www.rfc-editor.org/rfc/rfc7591#section-3.1)
45-
- [Jans Tarp](https://github.com/JanssenProject/jans/tree/main/demos/jans-tarp)
4646
4747
### Deployment
4848
@@ -69,11 +69,14 @@ Follow the steps below:
6969
"org.gluu.agama.typekey": {
7070
"keystoreName": "",
7171
"keystorePassword": "",
72-
"orgId": "",
73-
"clientId": "",
74-
"clientSecret": "",
72+
"orgId": "",
73+
"scan_ssa": "",
7574
"authHost": "https://account.gluu.org",
76-
"scanHost": "https://cloud.gluu.org"
75+
"scanHost": "https://cloud.gluu.org",
76+
"phrases": {
77+
"1": "itwasthebestoftimes",
78+
"2": "itwastheworstoftimes"
79+
}
7780
}
7881
}
7982
```
@@ -82,8 +85,9 @@ Follow the steps below:
8285
8386
- `keystoreName` and `keystorePassword` are optional, in case you want to include a signature when sending the Typekey data. Leave them as blank otherwise.
8487
- `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).
85-
- `clientId` and `clientSecret` are the client credentials obtained from Dynamic Client Registration
88+
- `scan_ssa` is the JWT string you obtain from Agama Lab
8689
- `authHost` and `scanHost` can be left as is
90+
- `phrases` is explained in the [Details](#details) section
8791
8892
- We go back to the TUI and click on `Import Configuration` and select the modified configuration file with our parameters.
8993
- 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)
96100
97101
Launch an authorization flow with parameters `acr_values=agama&agama_flow=org.gluu.agama.typekey` with your chosen RP.
98102
99-
Check out this video to see an example of **agama-typekey** in action:
103+
## Details
104+
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.
105+
In case Typekey API denies the request, Agama Typekey falls back to password authentication, and retrains the API on the provided data.
106+
107+
## Examples
108+
109+
Enrollment:
110+
111+
112+
https://github.com/SafinWasi/agama-typekey/assets/6601566/2256877b-3b49-48d8-b292-3d9da4a3a4c5
113+
114+
115+
116+
Typekey API approval:
117+
118+
119+
120+
https://github.com/SafinWasi/agama-typekey/assets/6601566/de5dcb19-9fbb-41f3-b897-606fc52fce85
121+
122+
123+
124+
125+
Typekey API denied, fallback to password:
126+
127+
128+
129+
https://github.com/SafinWasi/agama-typekey/assets/6601566/b0288f5c-6a84-4ea0-b6a4-ac9052409189
130+
131+
100132
101133
# Contributors
102134

code/org.gluu.agama.typekey.flow

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,49 @@
11
Flow org.gluu.agama.typekey
2-
Basepath ""
3-
Configs conf
2+
Basepath ""
3+
Configs conf
44
idp = Call org.gluu.agama.typekey.IdentityProcessor#new
55
tk = Call org.gluu.agama.typekey.Typekey#new conf
66
user = RRF "typekey/username.ftlh"
77
userData = Call idp accountFromUsername user.username
88
When userData.empty is true
9-
it_vsrve = {success:false, error: "User not found"}
10-
Finish it_vsrve
9+
it_vsrve = {success:false, error: "User not found"}
10+
Finish it_vsrve
11+
Call tk dynamicRegistration conf.scan_ssa
1112
enrolled = Call idp enrolled user.username
1213
When enrolled is false
13-
random_usecase = Call tk getRandomUseCase
14-
phrase_map = Call tk generateTypekeyData random_usecase
15-
dummy = Call idp enroll user.username phrase_map
16-
phrase = phrase_map.phrase
17-
use_case = random_usecase
14+
random_usecase = Call tk getRandomUseCase
15+
phrase_map = Call tk generateTypekeyData random_usecase
16+
dummy = Call idp enroll user.username phrase_map
17+
phrase = phrase_map.phrase
18+
use_case = random_usecase
1819
When enrolled is true
19-
typekey_data = Call idp getTypekeyData user.username
20-
phrase = typekey_data.phrase
21-
use_case = typekey_data.useCase
20+
typekey_data = Call idp getTypekeyData user.username
21+
phrase = typekey_data.phrase
22+
use_case = typekey_data.useCase
2223
phraseDict = {phrase:phrase}
2324
phraseData = RRF "typekey/phrase.ftlh" phraseDict
2425
typekey_result = Call tk validateKeystrokes user.username phraseData.phrase_data use_case
2526
When typekey_result.status is "Enrollment"
26-
password = RRF "typekey/password.ftlh"
27-
authenticated = Call idp authenticate user.username password.pwd
28-
When authenticated is true
29-
Call tk notifyKeystrokes user.username typekey_result.track_id use_case
30-
it_spikk = {success:true, data: { userId: user.username}}
31-
Finish it_spikk
32-
it_ttqbc = {success:false, error: "Authentication failed"}
33-
Finish it_ttqbc
27+
Log "Agama Typekey: Enrollment in progress"
28+
password = RRF "typekey/password.ftlh"
29+
authenticated = Call idp authenticate user.username password.pwd
30+
When authenticated is true
31+
Call tk notifyKeystrokes user.username typekey_result.track_id use_case
32+
it_spikk = {success:true, data: { userId: user.username}}
33+
Finish it_spikk
34+
it_ttqbc = {success:false, error: "Authentication failed"}
35+
Finish it_ttqbc
3436
When typekey_result.status is "Approved"
35-
it_zirls = {success:true, data: { userId: user.username}}
36-
Finish it_zirls
37+
Log "Agama Typekey: Approved"
38+
it_zirls = {success:true, data: { userId: user.username}}
39+
Finish it_zirls
3740
password = RRF "typekey/password.ftlh"
3841
authenticated = Call idp authenticate user.username password.pwd
3942
When authenticated is true
40-
When typekey_result.status is "Denied"
41-
Call tk notifyKeystrokes user.username typekey_result.track_id use_case
42-
it_becry = {success:true, data: { userId: user.username }}
43-
Finish it_becry
43+
When typekey_result.status is "Denied"
44+
Log "Denied, fell back to password"
45+
Call tk notifyKeystrokes user.username typekey_result.track_id use_case
46+
it_becry = {success:true, data: { userId: user.username }}
47+
Finish it_becry
4448
it_ryekg = {success:false, error: "Typekey and password failed"}
45-
Finish it_ryekg
49+
Finish it_ryekg

lib/org/gluu/agama/typekey/Typekey.java

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.jans.as.model.crypto.AuthCryptoProvider;
1212
import io.jans.service.cdi.util.CdiUtil;
1313
import io.jans.util.StringHelper;
14+
import io.jans.service.CacheService;
1415

1516
import java.net.URL;
1617
import java.net.URLEncoder;
@@ -70,6 +71,32 @@ private String buildServiceAuth() throws Exception {
7071
return "Bearer " + r.getContentAsJSONObject().getAsString("access_token");
7172
}
7273

74+
public void dynamicRegistration(String scanSSA) {
75+
String asEndpoint = config.getAuthHost() + "/jans-auth/restv1/register";
76+
HTTPRequest request = new HTTPRequest(HTTPRequest.Method.POST, new URL(asEndpoint));
77+
request.setAccept(APPLICATION_JSON);
78+
request.setContentType(APPLICATION_JSON);
79+
request.setConnectTimeout(3000);
80+
request.setReadTimeout(3000);
81+
JSONArray redirect_array = new JSONArray();
82+
redirect_array.put(config.getAuthHost());
83+
JSONArray grant_array = new JSONArray();
84+
grant_array.put("client_credentials");
85+
Map<String, Object> map = new HashMap(Map.of(
86+
"client_name", "typekey_client",
87+
"redirect_uris", redirect_array,
88+
"grant_types", grant_array,
89+
"software_statement", scanSSA,
90+
"lifetime", 86400));
91+
String message = new JSONObject(map).toString();
92+
request.setQuery(message);
93+
HTTPResponse r = request.send();
94+
r.ensureStatusCode(201);
95+
logger.info("Client registration successful");
96+
config.setClientId(r.getContentAsJSONObject().getAsString("client_id"));
97+
config.setClientSecret(r.getContentAsJSONObject().getAsString("client_secret"));
98+
}
99+
73100
private String signUid(String uid, String alias) throws Exception {
74101
AuthCryptoProvider auth = new AuthCryptoProvider(config.getKeystoreName(), config.getKeystorePassword(), null);
75102
String signedUid = auth.sign(uid, alias, null, SignatureAlgorithm.RS256);
@@ -96,43 +123,21 @@ public Map<String, Object> validateKeystrokes(String username, String k_data, St
96123
request.setQuery(message);
97124
request.setAuthorization(token);
98125
HTTPResponse r = request.send();
99-
Map<String, Object> responseObject;
100-
101-
if (r.getStatusCode() == 200) {
102-
responseObject = r.getContentAsJSONObject();
103-
return responseObject;
104-
} else {
105-
int statusCode = r.getStatusCode();
106-
responseObject = new HashMap<String, Object>();
107-
switch (statusCode) {
108-
case 401:
109-
responseObject.put("status", "Unauthorized");
110-
break;
111-
case 403:
112-
responseObject.put("status", "Forbidden");
113-
break;
114-
case 422:
115-
responseObject.put("status", "Unprocessable entity");
116-
break;
117-
case 400:
118-
responseObject.put("status", "Bad request");
119-
break;
120-
default:
121-
responseObject.put("status", "Other error");
122-
logger.info("Other error. Status code: {}", statusCode);
123-
break;
124-
}
125-
}
126+
127+
r.ensureStatusCode(200);
128+
Map<String, Object> responseObject = r.getContentAsJSONObject();
129+
126130
return responseObject;
127131
}
128132

129-
public void notifyKeystrokes(String uid, int track_id) {
133+
public void notifyKeystrokes(String uid, int track_id, String use_case) {
134+
int useCase = Integer.parseInt(use_case);
130135
String token = buildServiceAuth();
131136
Map<String, Object> map = new HashMap(Map.of(
132137
"uid", uid,
133138
"track_id", track_id,
134139
"org_id", config.getOrgId(),
135-
"use_case", 1));
140+
"use_case", useCase));
136141
String endpointUrl = config.getScanHost() + "/scan/typekey/notify";
137142
String message = new JSONObject(map).toString();
138143
logger.info(message);

project.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
"keystoreName": "",
1818
"keystorePassword": "",
1919
"orgId": "",
20-
"clientId": "",
21-
"clientSecret": "",
20+
"scan_ssa": "",
2221
"authHost": "https://account.gluu.org",
2322
"scanHost": "https://cloud.gluu.org",
2423
"phrases": {
@@ -28,4 +27,4 @@
2827
}
2928
},
3029
"name": "agama-typekey"
31-
}
30+
}

web/typekey/username.ftlh

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
1-
<#import "commons.ftlh" as com>
2-
<@com.main>
3-
<div class="border border-1 rounded p-5">
4-
<form id="login_form" method="POST">
5-
<div class="mb-3 row">
6-
<div class="col-md-8">
7-
<label for="username" class="col-md-4 col-form-label">Enter your username:</label>
8-
<input type="text" class="form-control" id="username" name="username">
9-
</div>
10-
</div>
11-
<div class="row">
12-
<div class="col-md-12 d-flex justify-content-end">
13-
<input type="submit" class="btn btn-success" id="login" value="Login">
14-
</div>
15-
</div>
16-
</form>
17-
</div>
18-
</@com.main>
1+
[#ftl output_format="HTML"]
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
<head></head>
5+
<body>[#import "commons.ftlh" as com]
6+
[@com.main]
7+
<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>
8+
[/@com.main]</body>
9+
10+
11+
</html>

0 commit comments

Comments
 (0)