Skip to content

Commit fe769f1

Browse files
Merge pull request #71 from privacyidea/passkey
passkey functions and constants
2 parents cfcbd0e + c348076 commit fe769f1

File tree

7 files changed

+235
-63
lines changed

7 files changed

+235
-63
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
echo "branches = ${{ steps.jacoco.outputs.branches }}"
4141
4242
- name: Upload JaCoCo coverage report as a workflow artifact
43-
uses: actions/upload-artifact@v3
43+
uses: actions/upload-artifact@v4
4444
with:
4545
name: jacoco-report
4646
path: target/site/jacoco/

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
<plugin>
4949
<groupId>org.apache.maven.plugins</groupId>
5050
<artifactId>maven-compiler-plugin</artifactId>
51-
<version>3.8.1</version>
51+
<version>3.13.0</version>
5252
<configuration>
5353
<source>11</source>
5454
<target>11</target>

src/main/java/org/privacyidea/Endpoint.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,21 +125,21 @@ void sendRequestAsync(String endpoint, Map<String, String> params, Map<String, S
125125
}
126126
privacyIDEA.log(method + " " + endpoint);
127127
params.forEach((k, v) ->
128-
{
128+
{
129129
if (k.equals("pass") || k.equals("password"))
130130
{
131131
v = "*".repeat(v.length());
132132
}
133133
privacyIDEA.log(k + "=" + v);
134-
});
134+
});
135135

136136
if (GET.equals(method))
137137
{
138138
params.forEach((key, value) ->
139-
{
139+
{
140140
String encValue = URLEncoder.encode(value, StandardCharsets.UTF_8);
141141
urlBuilder.addQueryParameter(key, encValue);
142-
});
142+
});
143143
}
144144

145145
String url = urlBuilder.build().toString();
@@ -157,7 +157,7 @@ void sendRequestAsync(String endpoint, Map<String, String> params, Map<String, S
157157
{
158158
FormBody.Builder formBodyBuilder = new FormBody.Builder();
159159
params.forEach((key, value) ->
160-
{
160+
{
161161
if (key != null && value != null)
162162
{
163163
String encValue = value;
@@ -169,13 +169,13 @@ void sendRequestAsync(String endpoint, Map<String, String> params, Map<String, S
169169
}
170170
formBodyBuilder.add(key, encValue);
171171
}
172-
});
172+
});
173173
// This switches okhttp to make a post request
174174
requestBuilder.post(formBodyBuilder.build());
175175
}
176176

177177
Request request = requestBuilder.build();
178-
//privacyIDEA.log("HEADERS:\n" + request.headers().toString());
178+
//privacyIDEA.log("HEADERS:\n" + request.headers());
179179
client.newCall(request).enqueue(callback);
180180
}
181181
}

src/main/java/org/privacyidea/JSONParser.java

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public PIResponse parsePIResponse(String serverResponse)
129129
if (result != null)
130130
{
131131
String r = getString(result, AUTHENTICATION);
132-
for (AuthenticationStatus as: AuthenticationStatus.values())
132+
for (AuthenticationStatus as : AuthenticationStatus.values())
133133
{
134134
if (as.toString().equals(r))
135135
{
@@ -168,14 +168,25 @@ else if ("interactive".equals(modeFromResponse))
168168
response.preferredClientMode = modeFromResponse;
169169
}
170170
response.message = getString(detail, MESSAGE);
171+
response.username = getString(detail, USERNAME);
171172
response.image = getString(detail, IMAGE);
172173
response.serial = getString(detail, SERIAL);
173174
response.transactionID = getString(detail, TRANSACTION_ID);
174175
response.type = getString(detail, TYPE);
175176
response.otpLength = getInt(detail, OTPLEN);
176-
177+
JsonObject passkeyChallenge = detail.getAsJsonObject(PASSKEY);
178+
if (passkeyChallenge != null && !passkeyChallenge.isJsonNull())
179+
{
180+
response.passkeyChallenge = passkeyChallenge.toString();
181+
// The passkey challenge can contain a transaction id, use that if none was set prior
182+
// This will happen if the passkey challenge was requested via /validate/initialize
183+
if (response.transactionID == null || response.transactionID.isEmpty())
184+
{
185+
response.transactionID = getString(passkeyChallenge, TRANSACTION_ID);
186+
}
187+
}
177188
String r = getString(detail, CHALLENGE_STATUS);
178-
for (ChallengeStatus cs: ChallengeStatus.values())
189+
for (ChallengeStatus cs : ChallengeStatus.values())
179190
{
180191
if (cs.toString().equals(r))
181192
{
@@ -187,12 +198,12 @@ else if ("interactive".equals(modeFromResponse))
187198
if (arrMessages != null)
188199
{
189200
arrMessages.forEach(val ->
190-
{
201+
{
191202
if (val != null)
192203
{
193204
response.messages.add(val.getAsString());
194205
}
195-
});
206+
});
196207
}
197208

198209
JsonArray arrChallenges = detail.getAsJsonArray(MULTI_CHALLENGE);
@@ -208,6 +219,17 @@ else if ("interactive".equals(modeFromResponse))
208219
String transactionID = getString(challenge, TRANSACTION_ID);
209220
String type = getString(challenge, TYPE);
210221

222+
if (challenge.has(PASSKEY_REGISTRATION))
223+
{
224+
response.passkeyRegistration = challenge.get(PASSKEY_REGISTRATION).toString();
225+
// TODO for passkey registration with enroll_via_multichallenge, the txid is probably in the wrong place
226+
// as of 3.11.0
227+
if (response.transactionID == null || response.transactionID.isEmpty())
228+
{
229+
response.transactionID = transactionID;
230+
}
231+
}
232+
211233
if (TOKEN_TYPE_WEBAUTHN.equals(type))
212234
{
213235
String webauthnSignRequest = getItemFromAttributes(WEBAUTHN_SIGN_REQUEST, challenge);
@@ -352,24 +374,24 @@ private TokenInfo parseSingleTokenInfo(String json)
352374
if (joInfo != null)
353375
{
354376
joInfo.entrySet().forEach(entry ->
355-
{
377+
{
356378
if (entry.getKey() != null && entry.getValue() != null)
357379
{
358380
info.info.put(entry.getKey(), entry.getValue().getAsString());
359381
}
360-
});
382+
});
361383
}
362384

363385
JsonArray arrRealms = obj.getAsJsonArray(REALMS);
364386
if (arrRealms != null)
365387
{
366388
arrRealms.forEach(val ->
367-
{
389+
{
368390
if (val != null)
369391
{
370392
info.realms.add(val.getAsString());
371393
}
372-
});
394+
});
373395
}
374396
return info;
375397
}
@@ -465,7 +487,7 @@ Map<String, String> parseWebAuthnSignResponse(String json)
465487
}
466488
catch (JsonSyntaxException e)
467489
{
468-
privacyIDEA.error("WebAuthn sign response has the wrong format: " + e.getLocalizedMessage());
490+
privacyIDEA.error("FIDO2 sign response has the wrong format: " + e.getLocalizedMessage());
469491
return null;
470492
}
471493

@@ -488,6 +510,40 @@ Map<String, String> parseWebAuthnSignResponse(String json)
488510
return params;
489511
}
490512

513+
Map<String, String> parseFIDO2AuthenticationResponse(String json)
514+
{
515+
Map<String, String> params = new LinkedHashMap<>();
516+
JsonObject obj;
517+
try
518+
{
519+
obj = JsonParser.parseString(json).getAsJsonObject();
520+
}
521+
catch (JsonSyntaxException e)
522+
{
523+
privacyIDEA.error("FIDO2 sign response has the wrong format: " + e.getLocalizedMessage());
524+
return null;
525+
}
526+
527+
params.put(CREDENTIAL_ID, getString(obj, CREDENTIAL_ID));
528+
params.put(CLIENTDATAJSON, getString(obj, CLIENTDATAJSON));
529+
params.put(SIGNATURE, getString(obj, SIGNATURE));
530+
params.put(AUTHENTICATOR_DATA, getString(obj, AUTHENTICATOR_DATA));
531+
532+
// The userhandle and assertionclientextension fields are optional
533+
String userhandle = getString(obj, USERHANDLE);
534+
if (!userhandle.isEmpty())
535+
{
536+
params.put(USERHANDLE, userhandle);
537+
}
538+
String extensions = getString(obj, ASSERTIONCLIENTEXTENSIONS);
539+
if (!extensions.isEmpty())
540+
{
541+
params.put(ASSERTIONCLIENTEXTENSIONS, extensions);
542+
}
543+
return params;
544+
}
545+
546+
491547
private boolean getBoolean(JsonObject obj, String name)
492548
{
493549
JsonPrimitive primitive = getPrimitiveOrNull(obj, name);
@@ -521,4 +577,26 @@ private JsonPrimitive getPrimitiveOrNull(JsonObject obj, String name)
521577
}
522578
return primitive;
523579
}
524-
}
580+
581+
public Map<String, String> parseFIDO2RegistrationResponse(String registrationResponse)
582+
{
583+
Map<String, String> params = new LinkedHashMap<>();
584+
JsonObject obj;
585+
try
586+
{
587+
obj = JsonParser.parseString(registrationResponse).getAsJsonObject();
588+
}
589+
catch (JsonSyntaxException e)
590+
{
591+
privacyIDEA.error("Passkey registration response is not JSON: " + e.getLocalizedMessage());
592+
return null;
593+
}
594+
595+
params.put(CREDENTIAL_ID, getString(obj, CREDENTIAL_ID));
596+
params.put(CLIENTDATAJSON, getString(obj, CLIENTDATAJSON));
597+
params.put(ATTESTATION_OBJECT, getString(obj, ATTESTATION_OBJECT));
598+
params.put(AUTHENTICATOR_ATTACHMENT, getString(obj, AUTHENTICATOR_ATTACHMENT));
599+
params.put(RAW_ID, getString(obj, RAW_ID));
600+
return params;
601+
}
602+
}

src/main/java/org/privacyidea/PIConstants.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@
2121

2222
public class PIConstants
2323
{
24-
private PIConstants()
25-
{
26-
}
27-
2824
public static final String GET = "GET";
2925
public static final String POST = "POST";
3026

@@ -34,6 +30,7 @@ private PIConstants()
3430
public static final String ENDPOINT_TRIGGERCHALLENGE = "/validate/triggerchallenge";
3531
public static final String ENDPOINT_POLLTRANSACTION = "/validate/polltransaction";
3632
public static final String ENDPOINT_VALIDATE_CHECK = "/validate/check";
33+
public static final String ENDPOINT_VALIDATE_INITIALIZE = "/validate/initialize";
3734
public static final String ENDPOINT_TOKEN = "/token/";
3835

3936
public static final String HEADER_ORIGIN = "Origin";
@@ -43,6 +40,7 @@ private PIConstants()
4340
// TOKEN TYPES
4441
public static final String TOKEN_TYPE_PUSH = "push";
4542
public static final String TOKEN_TYPE_WEBAUTHN = "webauthn";
43+
public static final String TOKEN_TYPE_PASSKEY = "passkey";
4644

4745
// JSON KEYS
4846
public static final String USERNAME = "username";
@@ -81,20 +79,28 @@ private PIConstants()
8179
public static final String ID = "id";
8280
public static final String MAXFAIL = "maxfail";
8381
public static final String INFO = "info";
82+
public static final String PASSKEY_REGISTRATION = "passkey_registration";
83+
public static final String AUTH_FORM = "authenticationForm";
84+
public static final String AUTH_FORM_RESULT = "authenticationFormResult";
8485

85-
// WebAuthn params
86+
// WebAuthn/Passkey params
8687
public static final String WEBAUTHN_SIGN_REQUEST = "webAuthnSignRequest";
8788
public static final String CREDENTIALID = "credentialid";
89+
public static final String CREDENTIAL_ID = "credential_id";
8890
public static final String CLIENTDATA = "clientdata";
91+
public static final String CLIENTDATAJSON = "clientDataJSON";
8992
public static final String SIGNATUREDATA = "signaturedata";
9093
public static final String AUTHENTICATORDATA = "authenticatordata";
94+
public static final String AUTHENTICATOR_DATA = "authenticatorData";
9195
public static final String USERHANDLE = "userhandle";
9296
public static final String ASSERTIONCLIENTEXTENSIONS = "assertionclientextensions";
93-
97+
public static final String PASSKEY = "passkey";
98+
public static final String RAW_ID = "rawId";
99+
public static final String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment";
100+
public static final String ATTESTATION_OBJECT = "attestationObject";
94101

95102
// These will be excluded from url encoding
96-
public static final List<String>
97-
WEBAUTHN_PARAMETERS =
98-
Arrays.asList(CREDENTIALID, CLIENTDATA, SIGNATUREDATA, AUTHENTICATORDATA, USERHANDLE,
99-
ASSERTIONCLIENTEXTENSIONS);
100-
}
103+
public static final List<String> WEBAUTHN_PARAMETERS = Arrays.asList(CREDENTIALID, CLIENTDATA, SIGNATUREDATA, AUTHENTICATORDATA,
104+
USERHANDLE, ASSERTIONCLIENTEXTENSIONS, CREDENTIAL_ID, RAW_ID,
105+
AUTHENTICATOR_ATTACHMENT, ATTESTATION_OBJECT);
106+
}

src/main/java/org/privacyidea/PIResponse.java

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ public class PIResponse
4949
public String type = ""; // Type of token that was matching the request
5050
public int otpLength = 0;
5151
public PIError error = null;
52+
// Passkey content is json string and can be passed to the browser as is
53+
public String passkeyChallenge = "";
54+
public String passkeyRegistration = "";
55+
public String username = "";
56+
57+
public boolean authenticationSuccessful()
58+
{
59+
if (authentication == AuthenticationStatus.ACCEPT)
60+
{
61+
return true;
62+
}
63+
else
64+
{
65+
return value && authentication != AuthenticationStatus.CHALLENGE;
66+
}
67+
}
5268

5369
/**
5470
* Check if a PUSH token was triggered.
@@ -83,12 +99,8 @@ public String otpMessage()
8399
private String reduceChallengeMessagesWhere(Predicate<Challenge> predicate)
84100
{
85101
StringBuilder sb = new StringBuilder();
86-
sb.append(multiChallenge.stream()
87-
.filter(predicate)
88-
.map(Challenge::getMessage)
89-
.distinct()
90-
.reduce("", (a, s) -> a + s + ", ")
91-
.trim());
102+
sb.append(
103+
multiChallenge.stream().filter(predicate).map(Challenge::getMessage).distinct().reduce("", (a, s) -> a + s + ", ").trim());
92104

93105
if (sb.length() > 0)
94106
{
@@ -113,16 +125,13 @@ public List<String> triggeredTokenTypes()
113125
public List<WebAuthn> webAuthnSignRequests()
114126
{
115127
List<WebAuthn> ret = new ArrayList<>();
116-
multiChallenge.stream()
117-
.filter(c -> TOKEN_TYPE_WEBAUTHN.equals(c.getType()))
118-
.collect(Collectors.toList())
119-
.forEach(c ->
120-
{
121-
if (c instanceof WebAuthn)
122-
{
123-
ret.add((WebAuthn) c);
124-
}
125-
});
128+
multiChallenge.stream().filter(c -> TOKEN_TYPE_WEBAUTHN.equals(c.getType())).collect(Collectors.toList()).forEach(c ->
129+
{
130+
if (c instanceof WebAuthn)
131+
{
132+
ret.add((WebAuthn) c);
133+
}
134+
});
126135
return ret;
127136
}
128137

@@ -164,4 +173,4 @@ public String toString()
164173
{
165174
return rawMessage;
166175
}
167-
}
176+
}

0 commit comments

Comments
 (0)