Use account-console client for server-side auth check

Also generate PKCE verifier and use challenge parameters

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
This commit is contained in:
Thomas Darimont 2024-10-15 21:15:48 +02:00 committed by Pedro Igor
parent 729417b20a
commit 40bdc902f0
3 changed files with 26 additions and 32 deletions

View file

@ -42,7 +42,6 @@ public class AbstractOAuthClient {
protected String stateCookiePath; protected String stateCookiePath;
protected boolean isSecure; protected boolean isSecure;
protected boolean publicClient; protected boolean publicClient;
protected boolean pkceEnabled;
protected String getStateCode() { protected String getStateCode() {
return counter.getAndIncrement() + "/" + UUID.randomUUID().toString(); return counter.getAndIncrement() + "/" + UUID.randomUUID().toString();
} }
@ -127,14 +126,6 @@ public class AbstractOAuthClient {
this.relativeUrlsUsed = relativeUrlsUsed; this.relativeUrlsUsed = relativeUrlsUsed;
} }
public boolean isPkceEnabled() {
return pkceEnabled;
}
public void setPkceEnabled(boolean pkceEnabled) {
this.pkceEnabled = pkceEnabled;
}
protected String stripOauthParametersFromRedirect(String uri) { protected String stripOauthParametersFromRedirect(String uri) {
KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(uri) KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(uri)
.replaceQueryParam(OAuth2Constants.CODE, null) .replaceQueryParam(OAuth2Constants.CODE, null)

View file

@ -28,7 +28,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.utils.PkceUtils;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
@ -45,7 +44,6 @@ import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.net.URI; import java.net.URI;
import java.util.Set; import java.util.Set;
import java.util.UUID;
/** /**
* Helper class for securing local services. Provides login basics as well as CSRF check basics * Helper class for securing local services. Provides login basics as well as CSRF check basics
@ -163,7 +161,7 @@ public abstract class AbstractSecuredLocalService {
return oauth.redirect(session.getContext().getUri(), accountUri.toString()); return oauth.redirect(session.getContext().getUri(), accountUri.toString());
} }
public static class OAuthRedirect extends AbstractOAuthClient { static class OAuthRedirect extends AbstractOAuthClient {
/** /**
* closes client * closes client
@ -182,20 +180,6 @@ public abstract class AbstractSecuredLocalService {
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.SCOPE, scopeParam); .queryParam(OAuth2Constants.SCOPE, scopeParam);
if (isPkceEnabled()) {
String pkceChallenge;
try {
// TODO generate PKCE challenge based on server value
String codeVerifier = UUID.randomUUID().toString();
pkceChallenge = PkceUtils.generateS256CodeChallenge(codeVerifier);
} catch (Exception e) {
throw new RuntimeException(e);
}
uriBuilder
.queryParam(OAuth2Constants.CODE_CHALLENGE, pkceChallenge)
.queryParam(OAuth2Constants.CODE_CHALLENGE_METHOD, OAuth2Constants.PKCE_METHOD_S256);
}
URI url = uriBuilder.build(); URI url = uriBuilder.build();
NewCookie cookie = new NewCookie(getStateCookieName(), state, getStateCookiePath(uriInfo), null, null, -1, isSecure, true); NewCookie cookie = new NewCookie(getStateCookieName(), state, getStateCookiePath(uriInfo), null, null, -1, isSecure, true);

View file

@ -6,10 +6,12 @@ import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriBuilder;
import org.jboss.resteasy.reactive.NoCache; import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.requiredactions.DeleteAccount; import org.keycloak.authentication.requiredactions.DeleteAccount;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.Version; import org.keycloak.common.Version;
import org.keycloak.common.util.Environment; import org.keycloak.common.util.Environment;
import org.keycloak.protocol.oidc.utils.PkceUtils;
import org.keycloak.utils.SecureContextResolver; import org.keycloak.utils.SecureContextResolver;
import org.keycloak.models.AccountRoles; import org.keycloak.models.AccountRoles;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -48,6 +50,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Scanner; import java.util.Scanner;
import java.util.UUID;
import java.util.function.Function; import java.util.function.Function;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -213,12 +216,28 @@ public class AccountConsole implements AccountResourceProvider {
} }
URI targetUri = consoleUriBuilder.build(realm.getName()); URI targetUri = consoleUriBuilder.build(realm.getName());
var oauthRedirect = new AbstractSecuredLocalService.OAuthRedirect(); String pkceChallenge;
oauthRedirect.setAuthUrl(OIDCLoginProtocolService.authUrl(session.getContext().getUri()).build(realm.getName()).toString()); try {
oauthRedirect.setClientId(Constants.ACCOUNT_CONSOLE_CLIENT_ID); // Add PKCE parameters as it is required for the account-console client.
oauthRedirect.setPkceEnabled(true); // Because the account console configuration requires PKCE, we need to send this with the redirect in order to not fail validations.
oauthRedirect.setSecure(realm.getSslRequired().isRequired(session.getContext().getConnection())); // The real PKCE challenge will be sent by the account-console OIDC client JavaScript integration.
return oauthRedirect.redirect(session.getContext().getUri(), targetUri.toString()); String codeVerifier = UUID.randomUUID().toString();
pkceChallenge = PkceUtils.generateS256CodeChallenge(codeVerifier);
} catch (Exception e) {
// this should never happen
throw new RuntimeException(e);
}
UriBuilder uriBuilder = UriBuilder.fromUri(OIDCLoginProtocolService.authUrl(session.getContext().getUri()).build(realm.getName()).toString())
.queryParam(OAuth2Constants.CLIENT_ID, Constants.ACCOUNT_CONSOLE_CLIENT_ID)
.queryParam(OAuth2Constants.REDIRECT_URI, targetUri)
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CODE_CHALLENGE, pkceChallenge)
.queryParam(OAuth2Constants.CODE_CHALLENGE_METHOD, OAuth2Constants.PKCE_METHOD_S256);
URI url = uriBuilder.build();
return Response.status(302).location(url).build();
} }
private Map<String, String> supportedLocales(Properties messages) { private Map<String, String> supportedLocales(Properties messages) {