Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

69 reuse saved jwt auth token until it expire 1 #70

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
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
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

{
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?

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