Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b95f47d
Update JSONParser.java
lukasmatusiewicz Dec 17, 2024
6f5e7ef
Update JSONParser.java
lukasmatusiewicz Dec 18, 2024
626a875
Update JSONParser.java
lukasmatusiewicz Dec 18, 2024
12ab544
Update PIConstants.java
lukasmatusiewicz Dec 18, 2024
2a90a4e
Update JSONParser.java
lukasmatusiewicz Dec 18, 2024
d581b23
Update JSONParser.java
lukasmatusiewicz Dec 18, 2024
872db86
Update PIConstants.java
lukasmatusiewicz Dec 18, 2024
0711b6e
Update PIResponse.java
lukasmatusiewicz Dec 18, 2024
098706d
Update PrivacyIDEA.java
lukasmatusiewicz Dec 18, 2024
072bf36
Update TestGetTokenInfo.java
lukasmatusiewicz Dec 19, 2024
c452b71
Update AsyncRequestCallable.java
lukasmatusiewicz Dec 19, 2024
26a3e90
Update AsyncRequestCallable.java
lukasmatusiewicz Dec 23, 2024
70bef18
Update PrivacyIDEA.java
lukasmatusiewicz Dec 23, 2024
f3bd138
Update PrivacyIDEA.java
lukasmatusiewicz Dec 31, 2024
f3736d0
Update JSONParser.java
lukasmatusiewicz Dec 31, 2024
01eb016
Update PrivacyIDEA.java
lukasmatusiewicz Dec 31, 2024
1e23c86
Update PrivacyIDEA.java
lukasmatusiewicz Dec 31, 2024
a6d491b
Update PrivacyIDEA.java
lukasmatusiewicz Jan 28, 2025
c09b902
update tests
lukasmatusiewicz Jan 28, 2025
faf235b
Update PrivacyIDEA.java
lukasmatusiewicz Feb 10, 2025
e6eb252
Update build.yml
lukasmatusiewicz Feb 10, 2025
9f77a94
Update PrivacyIDEA.java
lukasmatusiewicz Feb 10, 2025
3c5614f
Update JSONParser.java
lukasmatusiewicz Feb 12, 2025
81b52af
Update PrivacyIDEA.java
lukasmatusiewicz Feb 12, 2025
44cf2f8
Update pom.xml
lukasmatusiewicz Feb 12, 2025
7d3554f
Create TestJWTAuthToken.java
lukasmatusiewicz Feb 12, 2025
a94cad4
Update TestJWTAuthToken.java
lukasmatusiewicz Feb 12, 2025
d75e8cb
Update TestJWTAuthToken.java
lukasmatusiewicz Feb 12, 2025
479b918
Update TestValidateCheckSerial.java
lukasmatusiewicz Feb 12, 2025
b43e4f8
Update TestRollout.java
lukasmatusiewicz Feb 12, 2025
2629d18
update test, change naming
nilsbehlen Feb 13, 2025
e55ee8b
Update TestJWT.java
nilsbehlen Feb 13, 2025
5ba9e3e
Update TestJWT.java
lukasmatusiewicz Feb 13, 2025
f5e43c5
Update TestJWT.java
nilsbehlen Feb 13, 2025
437700b
schedule again when response is empty
nilsbehlen Feb 17, 2025
44a5fae
Merge branch 'master' into 69-reuse-saved-jwt-auth-token-until-it-exp…
lukasmatusiewicz Mar 26, 2025
3d50f1f
cleanup
lukasmatusiewicz Mar 26, 2025
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
31 changes: 3 additions & 28 deletions src/main/java/org/privacyidea/AsyncRequestCallable.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
Expand All @@ -35,7 +34,7 @@
*/
public class AsyncRequestCallable implements Callable<String>, Callback
{
private String path;
private final String path;
private final String method;
private final Map<String, String> headers;
private final Map<String, String> params;
Expand Down Expand Up @@ -63,32 +62,8 @@ public String call() throws Exception
// If an auth token is required for the request, get that first then do the actual request
if (this.authTokenRequired)
{
if (!privacyIDEA.serviceAccountAvailable())
{
privacyIDEA.error("Service account is required to retrieve auth token!");
return null;
}
latch = new CountDownLatch(1);
String tmpPath = path;
path = ENDPOINT_AUTH;
endpoint.sendRequestAsync(ENDPOINT_AUTH, privacyIDEA.serviceAccountParam(), Collections.emptyMap(), PIConstants.POST, this);
if (!latch.await(30, TimeUnit.SECONDS))
{
privacyIDEA.error("Latch timed out...");
return "";
}
// Extract the auth token from the response
String response = callbackResult[0];
String authToken = privacyIDEA.parser.extractAuthToken(response);
if (authToken == null)
{
// The parser already logs the error.
return null;
}
// Add the auth token to the header
headers.put(PIConstants.HEADER_AUTHORIZATION, authToken);
path = tmpPath;
callbackResult[0] = null;
// Wait for the auth token to be retrieved and add it to the header
headers.put(PIConstants.HEADER_AUTHORIZATION, privacyIDEA.getAuthToken());
}

// Do the actual request
Expand Down
34 changes: 21 additions & 13 deletions src/main/java/org/privacyidea/JSONParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@

import com.google.gson.*;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;

import static org.privacyidea.PIConstants.*;

Expand Down Expand Up @@ -66,9 +63,9 @@ public String formatJson(String json)
* Extract the auth token from the response of the server.
*
* @param serverResponse response of the server
* @return the auth token or null if error
* @return the AuthToken obj or null if error
*/
String extractAuthToken(String serverResponse)
LinkedHashMap<String, String> extractAuthToken(String serverResponse)
{
if (serverResponse != null && !serverResponse.isEmpty())
{
Expand All @@ -78,11 +75,22 @@ String extractAuthToken(String serverResponse)
try
{
JsonObject obj = root.getAsJsonObject();
return obj.getAsJsonObject(RESULT).getAsJsonObject(VALUE).getAsJsonPrimitive(TOKEN).getAsString();
String authToken = obj.getAsJsonObject(RESULT).getAsJsonObject(VALUE).getAsJsonPrimitive(TOKEN).getAsString();
var parts = authToken.split("\\.");
String dec = new String(Base64.getDecoder().decode(parts[1]));

// Extract the expiration date from the token
int respDate = obj.getAsJsonPrimitive(TIME).getAsInt();
int expDate = JsonParser.parseString(dec).getAsJsonObject().getAsJsonPrimitive(EXP).getAsInt();
int difference = expDate - respDate;
privacyIDEA.log("Authentication token expires in " + difference / 60 + " minutes.");

return new LinkedHashMap<>(Map.of(AUTH_TOKEN, authToken, AUTH_TOKEN_EXP, String.valueOf(expDate)));
}
catch (Exception e)
{
privacyIDEA.error("Response did not contain an authorization token: " + formatJson(serverResponse));
//privacyIDEA.error("Response did not contain an authorization token: " + formatJson(serverResponse));
privacyIDEA.error("Auth token extraction failed: " + e);
}
}
}
Expand Down Expand Up @@ -129,7 +137,7 @@ public PIResponse parsePIResponse(String serverResponse)
if (result != null)
{
String r = getString(result, AUTHENTICATION);
for (AuthenticationStatus as: AuthenticationStatus.values())
for (AuthenticationStatus as : AuthenticationStatus.values())
{
if (as.toString().equals(r))
{
Expand Down Expand Up @@ -175,7 +183,7 @@ else if ("interactive".equals(modeFromResponse))
response.otpLength = getInt(detail, OTPLEN);

String r = getString(detail, CHALLENGE_STATUS);
for (ChallengeStatus cs: ChallengeStatus.values())
for (ChallengeStatus cs : ChallengeStatus.values())
{
if (cs.toString().equals(r))
{
Expand Down Expand Up @@ -210,7 +218,7 @@ else if ("interactive".equals(modeFromResponse))

if (TOKEN_TYPE_WEBAUTHN.equals(type))
{
String webauthnSignRequest = getItemFromAttributes(WEBAUTHN_SIGN_REQUEST, challenge);
String webauthnSignRequest = getItemFromAttributes(challenge);
response.multiChallenge.add(new WebAuthn(serial, message, clientMode, image, transactionID, webauthnSignRequest));
}
else
Expand Down Expand Up @@ -241,13 +249,13 @@ static String mergeWebAuthnSignRequest(WebAuthn webauthn, List<String> arr) thro
return signRequest.toString();
}

private String getItemFromAttributes(String item, JsonObject jsonObject)
private String getItemFromAttributes(JsonObject jsonObject)
{
String ret = "";
JsonElement attributeElement = jsonObject.get(ATTRIBUTES);
if (attributeElement != null && !attributeElement.isJsonNull())
{
JsonElement requestElement = attributeElement.getAsJsonObject().get(item);
JsonElement requestElement = attributeElement.getAsJsonObject().get(PIConstants.WEBAUTHN_SIGN_REQUEST);
if (requestElement != null && !requestElement.isJsonNull())
{
ret = requestElement.toString();
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/privacyidea/PIConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ private PIConstants()
public static final String PASSWORD = "password";
public static final String PASS = "pass";
public static final String SERIAL = "serial";
public static final String TIME = "time";
public static final String EXP = "exp";
public static final String CHALLENGE_STATUS = "challenge_status";
public static final String AUTH_TOKEN = "authToken";
public static final String AUTH_TOKEN_EXP = "authTokenExp";
public static final String TYPE = "type";
public static final String TRANSACTION_ID = "transaction_id";
public static final String REALM = "realm";
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/privacyidea/PIResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static org.privacyidea.PIConstants.*;
import static org.privacyidea.PIConstants.TOKEN_TYPE_PUSH;
import static org.privacyidea.PIConstants.TOKEN_TYPE_WEBAUTHN;

/**
* This class parses the JSON response of privacyIDEA into a POJO for easier access.
Expand Down
101 changes: 73 additions & 28 deletions src/main/java/org/privacyidea/PrivacyIDEA.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ public class PrivacyIDEA implements Closeable
private final IPILogger log;
private final IPISimpleLogger simpleLog;
private final Endpoint endpoint;
private String authToken = null;
// Thread pool for connections
private final BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
private final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(20, 20, 10, TimeUnit.SECONDS, queue);
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private final CountDownLatch authTokenLatch = new CountDownLatch(1);
final JSONParser parser;
// Responses from these endpoints will not be logged. The list can be overwritten.
private List<String> logExcludedEndpoints = Arrays.asList(PIConstants.ENDPOINT_AUTH,
Expand All @@ -49,6 +52,10 @@ private PrivacyIDEA(PIConfig configuration, IPILogger logger, IPISimpleLogger si
this.endpoint = new Endpoint(this);
this.parser = new JSONParser(this);
this.threadPool.allowCoreThreadTimeOut(true);
if (serviceAccountAvailable())
{
retrieveAuthToken();
}
}

/**
Expand Down Expand Up @@ -150,6 +157,11 @@ private PIResponse getPIResponse(String type, String input, String pass, Map<Str
params.put(TRANSACTION_ID, transactionID);
}
String response = runRequestAsync(ENDPOINT_VALIDATE_CHECK, params, headers, false, POST);
// Shutdown the scheduler if user successfully authenticated
if (this.parser.parsePIResponse(response) != null && this.parser.parsePIResponse(response).value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not call parsePIResponse twice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right
faf235b

{
this.scheduler.shutdownNow();
}
return this.parser.parsePIResponse(response);
}

Expand Down Expand Up @@ -243,21 +255,10 @@ public ChallengeStatus pollTransaction(String transactionID)
}

/**
* Get the auth token from the /auth endpoint using the service account.
* Get the service account parameters.
*
* @return auth token or null.
* @return map with username and password.
*/
public String getAuthToken()
{
if (!serviceAccountAvailable())
{
error("Cannot retrieve auth token without service account!");
return null;
}
String response = runRequestAsync(ENDPOINT_AUTH, serviceAccountParam(), Collections.emptyMap(), false, POST);
return parser.extractAuthToken(response);
}

Map<String, String> serviceAccountParam()
{
Map<String, String> authTokenParams = new LinkedHashMap<>();
Expand Down Expand Up @@ -348,6 +349,11 @@ public RolloutInfo tokenInit(String username, String typeToEnroll, String otpKey
return parser.parseRolloutInfo(response);
}

/**
* Append the realm to the parameters if it is set.
*
* @param params parameters
*/
private void appendRealm(Map<String, String> params)
{
if (configuration.realm != null && !configuration.realm.isEmpty())
Expand All @@ -356,6 +362,48 @@ private void appendRealm(Map<String, String> params)
}
}

/**
* Retrieve the auth token from the /auth endpoint and schedule the next retrieval.
*/
private void retrieveAuthToken()
{
String response = runRequestAsync(ENDPOINT_AUTH, serviceAccountParam(), Collections.emptyMap(), false, POST);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

latch needs to be set again for this to work more than once?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinkedHashMap<String, String> authTokenMap = parser.extractAuthToken(response);
this.authToken = authTokenMap.get(AUTH_TOKEN);
int authTokenExp = Integer.parseInt(authTokenMap.get(AUTH_TOKEN_EXP));
log("Auth token expires in: " + (authTokenExp - System.currentTimeMillis() / 1000L) + " seconds.");

// Schedule the next token retrieval to 1 min before expiration
long delay = authTokenExp - 60 - System.currentTimeMillis() / 1000L;
scheduler.schedule(this::retrieveAuthToken, delay, TimeUnit.SECONDS);

// Count down the latch to indicate that the token is retrieved
authTokenLatch.countDown();
}

/**
* Get the auth token from the /auth endpoint using the service account.
*
* @return auth token or null.
* @throws InterruptedException if the thread is interrupted while waiting for the auth token.
*/
public String getAuthToken() throws InterruptedException
{
// Wait for the auth token to be retrieved
authTokenLatch.await();
return this.authToken;
}

/**
* @return true if a service account is available
*/
public boolean serviceAccountAvailable()
{
return configuration.serviceAccountName != null && !configuration.serviceAccountName.isEmpty()
&& configuration.serviceAccountPass != null &&
!configuration.serviceAccountPass.isEmpty();
}

/**
* Run a request in a thread of the thread pool. Then join that thread to the one that was calling this method.
* If the server takes longer to answer a request, the other requests do not have to wait.
Expand Down Expand Up @@ -388,6 +436,14 @@ private String runRequestAsync(String path, Map<String, String> params, Map<Stri
return response;
}

/**
* @return the configuration of this instance
*/
PIConfig configuration()
{
return configuration;
}

/**
* @return list of endpoints for which the response is not printed
*/
Expand All @@ -404,21 +460,6 @@ public void logExcludedEndpoints(List<String> list)
this.logExcludedEndpoints = list;
}

/**
* @return true if a service account is available
*/
public boolean serviceAccountAvailable()
{
return configuration.serviceAccountName != null && !configuration.serviceAccountName.isEmpty()
&& configuration.serviceAccountPass != null &&
!configuration.serviceAccountPass.isEmpty();
}

PIConfig configuration()
{
return configuration;
}

/**
* Pass the message to the appropriate logger implementation.
*
Expand Down Expand Up @@ -519,6 +560,7 @@ else if (this.simpleLog != null)
public void close() throws IOException
{
this.threadPool.shutdown();
this.scheduler.shutdownNow();
}

/**
Expand All @@ -533,6 +575,9 @@ public static Builder newBuilder(String serverURL, String userAgent)
return new Builder(serverURL, userAgent);
}

/**
* Builder class to create a PrivacyIDEA instance.
*/
public static class Builder
{
private final String serverURL;
Expand Down
Loading
Loading