Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

### 1.4.0 - 21 May 2025

* PIResponse class can return the transaction based on the mode/type, which currently are Push, WebAuthn, Passkey and OTP.
* HTTP request headers are logged
* WebAuthn class as derived class of Challenge has been removed to allow simple serialization of PIResponse
* allowCredentials for WebAuthnSignRequests are merged when the PIResponse object is created and the combined SignRequest
is set to PIResponse.webAuthnSignRequest. WebAuthn challenges are not in the multi_challenge list anymore!

### v1.3.1 - 14 May 2025

* PIResponse::isAuthenticationSuccessful will also consider if multi_challenge is present, not just the authentication field
Expand Down
41 changes: 32 additions & 9 deletions src/main/java/org/privacyidea/Challenge.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
package org.privacyidea;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Challenge
{
protected final List<String> attributes = new ArrayList<>();
protected final Map<String, String> attributes = new HashMap<>();
protected final String serial;
protected final String clientMode;
protected final String message;
Expand All @@ -39,17 +41,38 @@ public Challenge(String serial, String message, String clientMode, String image,
this.type = type;
}

public List<String> getAttributes() {return attributes;}
public Map<String, String> getAttributes()
{
return attributes;
}

public String getSerial() {return serial;}
public String getSerial()
{
return serial;
}

public String getMessage() {return message;}
public String getMessage()
{
return message;
}

public String getClientMode() {return clientMode;}
public String getClientMode()
{
return clientMode;
}

public String getImage() {return image.replaceAll("\"", "");}
public String getImage()
{
return image.replaceAll("\"", "");
}

public String getTransactionID() {return transactionID;}
public String getTransactionID()
{
return transactionID;
}

public String getType() {return type;}
}
public String getType()
{
return type;
}
}
2 changes: 1 addition & 1 deletion src/main/java/org/privacyidea/Endpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ void sendRequestAsync(String endpoint, Map<String, String> params, Map<String, S
}

Request request = requestBuilder.build();
//privacyIDEA.log("HEADERS:\n" + request.headers());
privacyIDEA.log("Header: " + request.headers().toString().replace("\n", " | "));
client.newCall(request).enqueue(callback);
}
}
20 changes: 16 additions & 4 deletions src/main/java/org/privacyidea/JSONParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ else if ("interactive".equals(modeFromResponse))
JsonArray arrChallenges = detail.getAsJsonArray(MULTI_CHALLENGE);
if (arrChallenges != null)
{
List<String> webauthnSignRequests = new ArrayList<>();
for (int i = 0; i < arrChallenges.size(); i++)
{
JsonObject challenge = arrChallenges.get(i).getAsJsonObject();
Expand All @@ -300,28 +301,39 @@ else if ("interactive".equals(modeFromResponse))
if (TOKEN_TYPE_WEBAUTHN.equals(type))
{
String webauthnSignRequest = getItemFromAttributes(challenge);
response.multiChallenge.add(new WebAuthn(serial, message, clientMode, image, transactionID, webauthnSignRequest));
response.webAuthnTransactionId = transactionID;
if (webauthnSignRequest != null && !webauthnSignRequest.isEmpty())
{
webauthnSignRequests.add(webauthnSignRequest);
}
}
else
{
response.multiChallenge.add(new Challenge(serial, message, clientMode, image, transactionID, type));
}
}
if (!webauthnSignRequests.isEmpty())
{
response.webAuthnSignRequest = mergeWebAuthnSignRequest(webauthnSignRequests);
}
}
}
return response;
}

static String mergeWebAuthnSignRequest(WebAuthn webauthn, List<String> arr) throws JsonSyntaxException
String mergeWebAuthnSignRequest(List<String> webAuthnSignRequests) throws JsonSyntaxException
{
String first = webAuthnSignRequests.get(0);
//webAuthnSignRequests.remove(0);

List<JsonArray> extracted = new ArrayList<>();
for (String signRequest : arr)
for (String signRequest : webAuthnSignRequests)
{
JsonObject obj = JsonParser.parseString(signRequest).getAsJsonObject();
extracted.add(obj.getAsJsonArray("allowCredentials"));
}

JsonObject signRequest = JsonParser.parseString(webauthn.signRequest()).getAsJsonObject();
JsonObject signRequest = JsonParser.parseString(first).getAsJsonObject();
JsonArray allowCredentials = new JsonArray();
extracted.forEach(allowCredentials::addAll);

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/privacyidea/PIConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public class PIConstants
public static final String SIGNATUREDATA = "signaturedata";
public static final String AUTHENTICATORDATA = "authenticatordata";
public static final String AUTHENTICATOR_DATA = "authenticatorData";
public static final String USERHANDLE = "userhandle";
public static final String USERHANDLE = "userHandle";
public static final String ASSERTIONCLIENTEXTENSIONS = "assertionclientextensions";
public static final String PASSKEY = "passkey";
public static final String RAW_ID = "rawId";
Expand Down
82 changes: 47 additions & 35 deletions src/main/java/org/privacyidea/PIResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
package org.privacyidea;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -55,6 +57,9 @@ public class PIResponse
public String username = "";
public String enrollmentLink = "";

public String webAuthnSignRequest = "";
public String webAuthnTransactionId = "";

public boolean authenticationSuccessful()
{
if (authentication == AuthenticationStatus.ACCEPT && (multiChallenge == null || multiChallenge.isEmpty()))
Expand Down Expand Up @@ -87,6 +92,29 @@ public String pushMessage()
return reduceChallengeMessagesWhere(c -> TOKEN_TYPE_PUSH.equals(c.getType()));
}

public String otpTransactionId()
{
for (Challenge challenge : multiChallenge)
{
if (!TOKEN_TYPE_PUSH.equals(challenge.getType()) && !TOKEN_TYPE_WEBAUTHN.equals(challenge.getType()))
{
return challenge.transactionID;
}
}
return null;
}

public String pushTransactionId() {
for (Challenge challenge : multiChallenge)
{
if (TOKEN_TYPE_PUSH.equals(challenge.getType()))
{
return challenge.transactionID;
}
}
return null;
}

/**
* Get the messages of all token that require an input field (HOTP, TOTP, SMS, Email...) reduced to a single string.
*
Expand Down Expand Up @@ -115,25 +143,12 @@ private String reduceChallengeMessagesWhere(Predicate<Challenge> predicate)
*/
public List<String> triggeredTokenTypes()
{
return multiChallenge.stream().map(Challenge::getType).distinct().collect(Collectors.toList());
}

/**
* Get all WebAuthn challenges from the multi_challenge.
*
* @return List of WebAuthn objects or empty list
*/
public List<WebAuthn> webAuthnSignRequests()
{
List<WebAuthn> ret = new ArrayList<>();
multiChallenge.stream().filter(c -> TOKEN_TYPE_WEBAUTHN.equals(c.getType())).collect(Collectors.toList()).forEach(c ->
{
if (c instanceof WebAuthn)
{
ret.add((WebAuthn) c);
}
});
return ret;
List<String> types = multiChallenge.stream().map(Challenge::getType).distinct().collect(Collectors.toList());
if (this.webAuthnSignRequest != null && !this.webAuthnSignRequest.isEmpty())
{
types.add(TOKEN_TYPE_WEBAUTHN);
}
return types;
}

/**
Expand All @@ -146,27 +161,24 @@ public List<WebAuthn> webAuthnSignRequests()
*/
public String mergedSignRequest()
{
List<WebAuthn> webauthnSignRequests = webAuthnSignRequests();
if (webauthnSignRequests.isEmpty())
if (this.webAuthnSignRequest == null || this.webAuthnSignRequest.isEmpty())
{
return "";
}
if (webauthnSignRequests.size() == 1)
{
return webauthnSignRequests.get(0).signRequest();
}
return this.webAuthnSignRequest;
}

WebAuthn webauthn = webauthnSignRequests.get(0);
List<String> stringSignRequests = webauthnSignRequests.stream().map(WebAuthn::signRequest).collect(Collectors.toList());
public String toJSON()
{
GsonBuilder builder = new GsonBuilder();
builder.setPrettyPrinting();
Gson gson = builder.create();
return gson.toJson(this);
}

try
{
return JSONParser.mergeWebAuthnSignRequest(webauthn, stringSignRequests);
}
catch (JsonSyntaxException e)
{
return "";
}
public static PIResponse fromJSON(String json)
{
return new Gson().fromJson(json, PIResponse.class);
}

@Override
Expand Down
39 changes: 0 additions & 39 deletions src/main/java/org/privacyidea/WebAuthn.java

This file was deleted.

Loading