2021-09-28 16:30:17 -07:00
|
|
|
package auth.sso.oidc.custom;
|
|
|
|
|
|
|
|
import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
|
|
|
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
|
|
|
|
import com.nimbusds.oauth2.sdk.ParseException;
|
|
|
|
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
|
|
|
|
import com.nimbusds.oauth2.sdk.TokenRequest;
|
|
|
|
import com.nimbusds.oauth2.sdk.TokenResponse;
|
|
|
|
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
|
|
|
|
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
|
|
|
|
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
|
|
|
|
import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
|
|
|
|
import com.nimbusds.oauth2.sdk.auth.Secret;
|
|
|
|
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
|
|
|
|
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
|
|
|
|
import com.nimbusds.oauth2.sdk.id.ClientID;
|
2022-12-21 20:11:11 -06:00
|
|
|
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
|
2021-09-28 16:30:17 -07:00
|
|
|
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
|
|
|
|
import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
|
2024-10-28 09:05:16 -05:00
|
|
|
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
2021-09-28 16:30:17 -07:00
|
|
|
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.net.URI;
|
|
|
|
import java.net.URISyntaxException;
|
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.Collection;
|
|
|
|
import java.util.List;
|
2024-10-28 09:05:16 -05:00
|
|
|
import java.util.Objects;
|
2021-09-28 16:30:17 -07:00
|
|
|
import java.util.Optional;
|
2024-10-28 09:05:16 -05:00
|
|
|
import org.pac4j.core.context.CallContext;
|
2021-09-28 16:30:17 -07:00
|
|
|
import org.pac4j.core.context.WebContext;
|
2024-10-28 09:05:16 -05:00
|
|
|
import org.pac4j.core.credentials.Credentials;
|
2021-09-28 16:30:17 -07:00
|
|
|
import org.pac4j.core.exception.TechnicalException;
|
|
|
|
import org.pac4j.core.util.CommonHelper;
|
|
|
|
import org.pac4j.oidc.client.OidcClient;
|
|
|
|
import org.pac4j.oidc.config.OidcConfiguration;
|
|
|
|
import org.pac4j.oidc.credentials.OidcCredentials;
|
|
|
|
import org.pac4j.oidc.credentials.authenticator.OidcAuthenticator;
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
2024-10-28 09:05:16 -05:00
|
|
|
public class CustomOidcAuthenticator extends OidcAuthenticator {
|
2021-09-28 16:30:17 -07:00
|
|
|
|
2024-10-28 09:05:16 -05:00
|
|
|
private static final Logger logger = LoggerFactory.getLogger(CustomOidcAuthenticator.class);
|
2021-09-28 16:30:17 -07:00
|
|
|
|
|
|
|
private static final Collection<ClientAuthenticationMethod> SUPPORTED_METHODS =
|
|
|
|
Arrays.asList(
|
|
|
|
ClientAuthenticationMethod.CLIENT_SECRET_POST,
|
|
|
|
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
|
|
|
|
ClientAuthenticationMethod.NONE);
|
|
|
|
|
2022-12-08 20:27:51 -06:00
|
|
|
private final ClientAuthentication clientAuthentication;
|
2021-09-28 16:30:17 -07:00
|
|
|
|
2024-10-28 09:05:16 -05:00
|
|
|
public CustomOidcAuthenticator(final OidcClient client) {
|
|
|
|
super(client.getConfiguration(), client);
|
2021-09-28 16:30:17 -07:00
|
|
|
|
|
|
|
// check authentication methods
|
2024-10-28 09:05:16 -05:00
|
|
|
OIDCProviderMetadata providerMetadata;
|
|
|
|
try {
|
|
|
|
providerMetadata = loadWithRetry();
|
|
|
|
} catch (TechnicalException e) {
|
|
|
|
logger.error(
|
|
|
|
"Could not resolve identity provider's remote configuration from DiscoveryURI: {}",
|
|
|
|
configuration.getDiscoveryURI());
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<ClientAuthenticationMethod> metadataMethods =
|
|
|
|
providerMetadata.getTokenEndpointAuthMethods();
|
2021-09-28 16:30:17 -07:00
|
|
|
|
2023-12-06 11:02:42 +05:30
|
|
|
final ClientAuthenticationMethod preferredMethod =
|
|
|
|
getPreferredAuthenticationMethod(configuration);
|
2021-09-28 16:30:17 -07:00
|
|
|
|
|
|
|
final ClientAuthenticationMethod chosenMethod;
|
|
|
|
if (CommonHelper.isNotEmpty(metadataMethods)) {
|
|
|
|
if (preferredMethod != null) {
|
2023-12-06 11:02:42 +05:30
|
|
|
if (ClientAuthenticationMethod.NONE.equals(preferredMethod)
|
|
|
|
|| metadataMethods.contains(preferredMethod)) {
|
2021-09-28 16:30:17 -07:00
|
|
|
chosenMethod = preferredMethod;
|
|
|
|
} else {
|
|
|
|
throw new TechnicalException(
|
2022-05-10 18:15:53 -05:00
|
|
|
"Preferred authentication method ("
|
|
|
|
+ preferredMethod
|
|
|
|
+ ") not supported "
|
|
|
|
+ "by provider according to provider metadata ("
|
|
|
|
+ metadataMethods
|
|
|
|
+ ").");
|
2021-09-28 16:30:17 -07:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
chosenMethod = firstSupportedMethod(metadataMethods);
|
|
|
|
}
|
|
|
|
} else {
|
2023-12-06 11:02:42 +05:30
|
|
|
chosenMethod =
|
|
|
|
preferredMethod != null ? preferredMethod : ClientAuthenticationMethod.getDefault();
|
|
|
|
logger.info(
|
|
|
|
"Provider metadata does not provide Token endpoint authentication methods. Using: {}",
|
2021-09-28 16:30:17 -07:00
|
|
|
chosenMethod);
|
|
|
|
}
|
|
|
|
|
2022-05-10 18:15:53 -05:00
|
|
|
final ClientID clientID = new ClientID(configuration.getClientId());
|
2022-04-26 17:01:18 -04:00
|
|
|
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(chosenMethod)) {
|
2022-05-10 18:15:53 -05:00
|
|
|
final Secret secret = new Secret(configuration.getSecret());
|
|
|
|
clientAuthentication = new ClientSecretPost(clientID, secret);
|
2021-09-28 16:30:17 -07:00
|
|
|
} else if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(chosenMethod)) {
|
2022-05-10 18:15:53 -05:00
|
|
|
final Secret secret = new Secret(configuration.getSecret());
|
|
|
|
clientAuthentication = new ClientSecretBasic(clientID, secret);
|
2022-04-26 17:01:18 -04:00
|
|
|
} else if (ClientAuthenticationMethod.NONE.equals(chosenMethod)) {
|
|
|
|
clientAuthentication = null; // No client authentication in none mode
|
2021-09-28 16:30:17 -07:00
|
|
|
} else {
|
|
|
|
throw new TechnicalException("Unsupported client authentication method: " + chosenMethod);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-12-06 11:02:42 +05:30
|
|
|
* The preferred {@link ClientAuthenticationMethod} specified in the given {@link
|
|
|
|
* OidcConfiguration}, or <code>null</code> meaning that the a provider-supported method should be
|
|
|
|
* chosen.
|
2021-09-28 16:30:17 -07:00
|
|
|
*/
|
2023-12-06 11:02:42 +05:30
|
|
|
private static ClientAuthenticationMethod getPreferredAuthenticationMethod(
|
|
|
|
OidcConfiguration config) {
|
2021-09-28 16:30:17 -07:00
|
|
|
final ClientAuthenticationMethod configurationMethod = config.getClientAuthenticationMethod();
|
|
|
|
if (configurationMethod == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!SUPPORTED_METHODS.contains(configurationMethod)) {
|
2023-12-06 11:02:42 +05:30
|
|
|
throw new TechnicalException(
|
|
|
|
"Configured authentication method (" + configurationMethod + ") is not supported.");
|
2021-09-28 16:30:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return configurationMethod;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-12-06 11:02:42 +05:30
|
|
|
* The first {@link ClientAuthenticationMethod} from the given list of methods that is supported
|
|
|
|
* by this implementation.
|
2021-09-28 16:30:17 -07:00
|
|
|
*
|
2023-12-06 11:02:42 +05:30
|
|
|
* @throws TechnicalException if none of the provider-supported methods is supported.
|
2021-09-28 16:30:17 -07:00
|
|
|
*/
|
2023-12-06 11:02:42 +05:30
|
|
|
private static ClientAuthenticationMethod firstSupportedMethod(
|
|
|
|
final List<ClientAuthenticationMethod> metadataMethods) {
|
2021-09-28 16:30:17 -07:00
|
|
|
Optional<ClientAuthenticationMethod> firstSupported =
|
|
|
|
metadataMethods.stream().filter((m) -> SUPPORTED_METHODS.contains(m)).findFirst();
|
|
|
|
if (firstSupported.isPresent()) {
|
|
|
|
return firstSupported.get();
|
|
|
|
} else {
|
2023-12-06 11:02:42 +05:30
|
|
|
throw new TechnicalException(
|
|
|
|
"None of the Token endpoint provider metadata authentication methods are supported: "
|
|
|
|
+ metadataMethods);
|
2021-09-28 16:30:17 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2024-10-28 09:05:16 -05:00
|
|
|
public Optional<Credentials> validate(CallContext ctx, Credentials cred) {
|
|
|
|
OidcCredentials credentials = (OidcCredentials) cred;
|
|
|
|
WebContext context = ctx.webContext();
|
|
|
|
|
|
|
|
final AuthorizationCode code = credentials.toAuthorizationCode();
|
2021-09-28 16:30:17 -07:00
|
|
|
// if we have a code
|
|
|
|
if (code != null) {
|
|
|
|
try {
|
|
|
|
final String computedCallbackUrl = client.computeFinalCallbackUrl(context);
|
2023-12-06 11:02:42 +05:30
|
|
|
CodeVerifier verifier =
|
|
|
|
(CodeVerifier)
|
|
|
|
configuration
|
|
|
|
.getValueRetriever()
|
2024-10-28 09:05:16 -05:00
|
|
|
.retrieve(ctx, client.getCodeVerifierSessionAttributeName(), client)
|
2023-12-06 11:02:42 +05:30
|
|
|
.orElse(null);
|
2021-09-28 16:30:17 -07:00
|
|
|
// Token request
|
2023-12-06 11:02:42 +05:30
|
|
|
final TokenRequest request =
|
|
|
|
createTokenRequest(
|
|
|
|
new AuthorizationCodeGrant(code, new URI(computedCallbackUrl), verifier));
|
2021-09-28 16:30:17 -07:00
|
|
|
HTTPRequest tokenHttpRequest = request.toHTTPRequest();
|
|
|
|
tokenHttpRequest.setConnectTimeout(configuration.getConnectTimeout());
|
|
|
|
tokenHttpRequest.setReadTimeout(configuration.getReadTimeout());
|
|
|
|
|
|
|
|
final HTTPResponse httpResponse = tokenHttpRequest.send();
|
2023-12-06 11:02:42 +05:30
|
|
|
logger.debug(
|
|
|
|
"Token response: status={}, content={}",
|
|
|
|
httpResponse.getStatusCode(),
|
2021-09-28 16:30:17 -07:00
|
|
|
httpResponse.getContent());
|
|
|
|
|
|
|
|
final TokenResponse response = OIDCTokenResponseParser.parse(httpResponse);
|
|
|
|
if (response instanceof TokenErrorResponse) {
|
2023-12-06 11:02:42 +05:30
|
|
|
throw new TechnicalException(
|
|
|
|
"Bad token response, error=" + ((TokenErrorResponse) response).getErrorObject());
|
2021-09-28 16:30:17 -07:00
|
|
|
}
|
|
|
|
logger.debug("Token response successful");
|
|
|
|
final OIDCTokenResponse tokenSuccessResponse = (OIDCTokenResponse) response;
|
|
|
|
|
|
|
|
// save tokens in credentials
|
|
|
|
final OIDCTokens oidcTokens = tokenSuccessResponse.getOIDCTokens();
|
2024-10-28 09:05:16 -05:00
|
|
|
credentials.setAccessTokenObject(oidcTokens.getAccessToken());
|
|
|
|
|
|
|
|
// Only set refresh token if it exists
|
|
|
|
if (oidcTokens.getRefreshToken() != null) {
|
|
|
|
credentials.setRefreshTokenObject(oidcTokens.getRefreshToken());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (oidcTokens.getIDToken() != null) {
|
|
|
|
credentials.setIdToken(oidcTokens.getIDToken().getParsedString());
|
|
|
|
}
|
2021-09-28 16:30:17 -07:00
|
|
|
|
|
|
|
} catch (final URISyntaxException | IOException | ParseException e) {
|
|
|
|
throw new TechnicalException(e);
|
|
|
|
}
|
|
|
|
}
|
2024-10-28 09:05:16 -05:00
|
|
|
|
|
|
|
return Optional.ofNullable(cred);
|
2021-09-28 16:30:17 -07:00
|
|
|
}
|
|
|
|
|
2024-10-28 09:05:16 -05:00
|
|
|
// Simple retry with exponential backoff
|
|
|
|
public OIDCProviderMetadata loadWithRetry() {
|
|
|
|
int maxAttempts = 3;
|
|
|
|
long initialDelay = 1000; // 1 second
|
|
|
|
|
|
|
|
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
|
|
try {
|
|
|
|
OIDCProviderMetadata providerMetadata = configuration.getOpMetadataResolver().load();
|
|
|
|
return Objects.requireNonNull(providerMetadata);
|
|
|
|
} catch (RuntimeException e) {
|
|
|
|
if (attempt == maxAttempts) {
|
|
|
|
throw e; // Rethrow on final attempt
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
// Exponential backoff
|
|
|
|
Thread.sleep(initialDelay * (long) Math.pow(2, attempt - 1));
|
|
|
|
} catch (InterruptedException ie) {
|
|
|
|
Thread.currentThread().interrupt();
|
|
|
|
throw new RuntimeException("Retry interrupted", ie);
|
|
|
|
}
|
|
|
|
logger.warn("Retry attempt {} of {} failed", attempt, maxAttempts, e);
|
|
|
|
}
|
2022-04-26 17:01:18 -04:00
|
|
|
}
|
2024-10-28 09:05:16 -05:00
|
|
|
throw new RuntimeException(
|
|
|
|
"Failed to load provider metadata after " + maxAttempts + " attempts");
|
2021-09-28 16:30:17 -07:00
|
|
|
}
|
|
|
|
}
|