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

PKCE Support #6

Merged
merged 3 commits into from
Mar 18, 2024
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
120 changes: 79 additions & 41 deletions README.md

Large diffs are not rendered by default.

58 changes: 42 additions & 16 deletions src/main/java/io/okdp/spark/authc/OidcAuthFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static io.okdp.spark.authc.utils.HttpAuthenticationUtils.domain;
import static io.okdp.spark.authc.utils.HttpAuthenticationUtils.sendError;
import static io.okdp.spark.authc.utils.PreconditionsUtils.assertCookieSecure;
import static io.okdp.spark.authc.utils.PreconditionsUtils.assertSupportePKCE;
import static io.okdp.spark.authc.utils.PreconditionsUtils.assertSupportedScopes;
import static io.okdp.spark.authc.utils.PreconditionsUtils.checkAuthLogin;
import static io.okdp.spark.authc.utils.TokenUtils.userInfo;
Expand All @@ -34,15 +35,20 @@
import io.okdp.spark.authc.model.UserInfo;
import io.okdp.spark.authc.model.WellKnownConfiguration;
import io.okdp.spark.authc.provider.AuthProvider;
import io.okdp.spark.authc.provider.store.CookieTokenStore;
import io.okdp.spark.authc.provider.impl.store.CookieSessionStore;
import io.okdp.spark.authc.utils.HttpAuthenticationUtils;
import io.okdp.spark.authc.utils.JsonUtils;
import io.okdp.spark.authc.utils.PreconditionsUtils;
import io.okdp.spark.authc.utils.exception.Try;
import io.okdp.spark.authz.OidcGroupMappingServiceProvider;
import java.io.IOException;
import java.util.Optional;
import javax.servlet.*;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
Expand All @@ -51,11 +57,11 @@

@Slf4j
public class OidcAuthFilter implements Filter, Constants {

private AuthProvider authProvider;

@Override
public void init(FilterConfig filterConfig) {

String issuerUri =
PreconditionsUtils.checkNotNull(
ofNullable(filterConfig.getInitParameter(AUTH_ISSUER_URI))
Expand All @@ -67,10 +73,8 @@ public void init(FilterConfig filterConfig) {
.orElse(System.getenv("AUTH_CLIENT_ID")),
AUTH_CLIENT_ID);
String clientSecret =
PreconditionsUtils.checkNotNull(
ofNullable(filterConfig.getInitParameter(AUTH_CLIENT_SECRET))
.orElse(System.getenv("AUTH_CLIENT_SECRET")),
AUTH_CLIENT_SECRET);
ofNullable(filterConfig.getInitParameter(AUTH_CLIENT_SECRET))
.orElse(System.getenv("AUTH_CLIENT_SECRET"));
String redirectUri =
PreconditionsUtils.checkNotNull(
ofNullable(filterConfig.getInitParameter(AUTH_REDIRECT_URI))
Expand Down Expand Up @@ -98,6 +102,9 @@ public void init(FilterConfig filterConfig) {
.orElse(
ofNullable(System.getenv("AUTH_COOKE_MAX_AGE_SECONDS"))
.orElse(String.valueOf(AUTH_COOKE_DEFAULT_MAX_AGE_MINUTES))));
String usePKCE =
ofNullable(filterConfig.getInitParameter(AUTH_USE_PKCE))
.orElse(ofNullable(System.getenv("AUTH_USE_PKCE")).orElse("auto"));

log.info(
"Initializing OIDC Auth filter ({}: <{}>, {}: <{}>) ...",
Expand All @@ -106,6 +113,17 @@ public void init(FilterConfig filterConfig) {
AUTH_CLIENT_ID,
clientId);

ofNullable(clientSecret)
.ifPresentOrElse(
secret ->
log.info(
"Client Secret provided - Running with Confidential Client with PKCE support set to '{}'",
usePKCE),
() ->
log.info(
"Client Secret not provided - Running with Public Client with PKCE support set to '{}'",
usePKCE));

OidcConfig oidcConfig =
OidcConfig.builder()
.issuerUri(issuerUri)
Expand All @@ -114,6 +132,7 @@ public void init(FilterConfig filterConfig) {
.redirectUri(redirectUri)
.responseType("code")
.scope(scope)
.usePKCE(usePKCE)
.wellKnownConfiguration(
JsonUtils.loadJsonFromUrl(
format("%s%s", issuerUri, AUTH_ISSUER_WELL_KNOWN_CONFIGURATION),
Expand All @@ -125,11 +144,13 @@ public void init(FilterConfig filterConfig) {
+ "Authorization Endpoint: {}, \n"
+ "Token Endpoint: {}, \n"
+ "User Info Endpoint: {}, \n"
+ "Supported Scopes: {}",
+ "Supported Scopes: {}, \n"
+ "PKCE Supported Code Challenge Methods: {}, \n",
oidcConfig.wellKnownConfiguration().authorizationEndpoint(),
oidcConfig.wellKnownConfiguration().tokenEndpoint(),
oidcConfig.wellKnownConfiguration().userInfoEndpoint(),
oidcConfig.wellKnownConfiguration().scopesSupported());
oidcConfig.wellKnownConfiguration().scopesSupported(),
oidcConfig.wellKnownConfiguration().supportedPKCECodeChallengeMethods());

assertSupportedScopes(
oidcConfig.wellKnownConfiguration().scopesSupported(),
Expand All @@ -139,17 +160,22 @@ public void init(FilterConfig filterConfig) {
oidcConfig.redirectUri(),
isCookieSecure,
format("%s|env: %s", AUTH_COOKE_IS_SECURE, "AUTH_COOKE_IS_SECURE"));
assertSupportePKCE(
oidcConfig.wellKnownConfiguration().supportedPKCECodeChallengeMethods(),
usePKCE,
clientSecret,
format("%s|env: %s", AUTH_CLIENT_SECRET, "AUTH_COOKE_IS_SECURE"));

log.info(
"Initializing OIDC Auth Provider (access token cookie based storage/cookie name: {},"
"Initializing OIDC Auth Provider (Cookie based storage for High Available session persistence/cookie name: {},"
+ " max-age (minutes): {}) ...",
AUTH_COOKE_NAME,
cookieMaxAgeMinutes);
authProvider =
HttpSecurityConfig.create(oidcConfig)
.authorizeRequests(".*/.*\\.css", ".*/.*\\.js", ".*/.*\\.png")
.tokenStore(
CookieTokenStore.of(
.sessionStore(
CookieSessionStore.of(
AUTH_COOKE_NAME,
domain(oidcConfig.redirectUri()),
isCookieSecure,
Expand All @@ -173,7 +199,7 @@ public void doFilter(
HttpAuthenticationUtils.getCookieValue(AUTH_COOKE_NAME, servletRequest);
if (maybeAuthCookie.isPresent()) {
PersistedToken persistedToken =
authProvider.httpSecurityConfig().tokenStore().readToken(maybeAuthCookie.get());
authProvider.httpSecurityConfig().sessionStore().readToken(maybeAuthCookie.get());

if (persistedToken.isExpired()) {
log.info("The user {} token was expired, renewing ... ", persistedToken.userInfo().email());
Expand All @@ -191,7 +217,7 @@ public void doFilter(
log.info(
"Unable to renew access token from refresh token, removing cookie and retrying ....., cause: {}",
e.getMessage()));
Cookie cookie = authProvider.httpSecurityConfig().tokenStore().save(accessToken);
Cookie cookie = authProvider.httpSecurityConfig().sessionStore().save(accessToken);
((HttpServletResponse) servletResponse).addCookie(cookie);
}
// Add the user and groups in the user/group mappings authorization cache
Expand Down Expand Up @@ -219,7 +245,7 @@ public void doFilter(
// Exchange the obtained 'code' with an access token by issuing a request against the oidc
// provider
AccessToken accessToken =
Try.of(() -> authProvider.requestAccessToken(maybeAuthzCode.get()))
Try.of(() -> authProvider.requestAccessToken(servletRequest, servletResponse))
.onException(e -> sendError(servletResponse, e.getHttpStatusCode(), e.getMessage()));
UserInfo userInfo = userInfo(accessToken.accessToken());
log.info(
Expand All @@ -240,7 +266,7 @@ public void doFilter(
+ "Please try to delete your oidc provider cookie from the browser and try again!")))
.onException(e -> sendError(servletResponse, e.getHttpStatusCode(), e.getMessage()));

Cookie cookie = authProvider.httpSecurityConfig().tokenStore().save(accessToken);
Cookie cookie = authProvider.httpSecurityConfig().sessionStore().save(accessToken);
((HttpServletResponse) servletResponse).addCookie(cookie);
// Add the user and groups in the user/group mappings authorization cache
OidcGroupMappingServiceProvider.addUserAndGroups(
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/io/okdp/spark/authc/config/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public interface Constants {
/** The http cookie name where the access token is saved */
String AUTH_COOKE_NAME = "OKDP_AUTH_SPARK_UI";

/** The http auth state cookie name which holds the auth state and PKCE data */
String AUTH_STATE_COOKE_NAME = AUTH_COOKE_NAME + "_STATE";

/** Transmit the cookie over HTTPS only */
String AUTH_COOKE_IS_SECURE = "cookie-is-secure";

Expand All @@ -57,6 +60,9 @@ public interface Constants {
/** The default cookie expiration period minutes */
int AUTH_COOKE_DEFAULT_MAX_AGE_MINUTES = 12 * 60;

/** Use PKCE (true|false|auto) */
String AUTH_USE_PKCE = "use-pkce";

/** The cookie encryption key parameter name */
String AUTH_COOKIE_ENCRYPTION_KEY = "cookie-cipher-secret-key";

Expand Down
33 changes: 24 additions & 9 deletions src/main/java/io/okdp/spark/authc/config/HttpSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
import static java.util.Arrays.stream;

import io.okdp.spark.authc.provider.AuthProvider;
import io.okdp.spark.authc.provider.OidcAuthProvider;
import io.okdp.spark.authc.provider.TokenStore;
import io.okdp.spark.authc.provider.SessionStore;
import io.okdp.spark.authc.provider.impl.DefaultAuthorizationCodeAuthProvider;
import io.okdp.spark.authc.provider.impl.PKCEAuthorizationCodeAuthProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
Expand All @@ -29,15 +30,17 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

@RequiredArgsConstructor(staticName = "create")
@Getter
@Accessors(fluent = true)
@Slf4j
public class HttpSecurityConfig {

private final List<Pattern> patterns = new ArrayList<>();
@NonNull private OidcConfig oidcConfig;
private TokenStore tokenStore;
private SessionStore sessionStore;

/**
* Skip authentication for the requests with the provided URL patterns
Expand All @@ -50,17 +53,29 @@ public HttpSecurityConfig authorizeRequests(String... patterns) {
}

/**
* The token store {@link TokenStore} implementation managing the access token persistence
* The token store {@link SessionStore} implementation managing the access token persistence
*
* @see io.okdp.spark.authc.provider.TokenStore
* @see SessionStore
*/
public HttpSecurityConfig tokenStore(TokenStore tokenStore) {
this.tokenStore = tokenStore;
public HttpSecurityConfig sessionStore(SessionStore sessionStore) {
this.sessionStore = sessionStore;
return this;
}

/** Configure the security rules */
/** Configure the auth provider */
public AuthProvider configure() {
return new OidcAuthProvider(this);
switch (oidcConfig.usePKCE().toLowerCase()) {
case "true":
return new PKCEAuthorizationCodeAuthProvider(this);
case "auto":
List<String> supportedMethods =
oidcConfig.wellKnownConfiguration().supportedPKCECodeChallengeMethods();
return !supportedMethods.isEmpty()
? new PKCEAuthorizationCodeAuthProvider(this)
: new DefaultAuthorizationCodeAuthProvider(this);
case "false":
default:
return new DefaultAuthorizationCodeAuthProvider(this);
}
}
}
1 change: 1 addition & 0 deletions src/main/java/io/okdp/spark/authc/config/OidcConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ public class OidcConfig {
private String responseType;
private String scope;
private WellKnownConfiguration wellKnownConfiguration;
private String usePKCE;
}
13 changes: 13 additions & 0 deletions src/main/java/io/okdp/spark/authc/model/AccessToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@

package io.okdp.spark.authc.model;

import static java.time.Instant.now;
import static java.util.Date.from;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.okdp.spark.authc.utils.TokenUtils;
import lombok.Data;
import lombok.experimental.Accessors;

Expand All @@ -43,4 +47,13 @@ public class AccessToken {

@JsonProperty("id_token")
private String idToken;

public PersistedToken toPersistedToken() {
return PersistedToken.builder()
.userInfo(TokenUtils.userInfo(accessToken))
.refreshToken(refreshToken)
.expiresIn(expiresIn)
.expiresAt(from(now().plusSeconds(expiresIn)))
.build();
}
}
86 changes: 86 additions & 0 deletions src/main/java/io/okdp/spark/authc/model/AuthState.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2024 tosit.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.okdp.spark.authc.model;

import static java.nio.charset.StandardCharsets.US_ASCII;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.okdp.spark.authc.exception.OidcClientException;
import io.okdp.spark.authc.utils.JsonUtils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@Accessors(fluent = true)
@JsonIgnoreProperties(ignoreUnknown = true)
@AllArgsConstructor
@NoArgsConstructor
public class AuthState {

@JsonProperty("state")
private String state;

@JsonProperty("code_verifier")
private String codeVerifier;

@JsonProperty("code_challenge")
private String codeChallenge;

/** Converts this object to json string */
public String toJson() {
return JsonUtils.toJson(this);
}

/**
* Generates a random state containing: state: a random string code_verifier: A random string
* which conform to <a href="https://tools.ietf.org/html/rfc7636#section-4.1">specification</a>
* code_challenge: derived from the code_verifier
*/
public static AuthState randomState() {
String state = randomString(16);
String codeVerifier = randomString(64);
String codeChallenge = createCodeChallenge(codeVerifier);
return new AuthState(state, codeVerifier, codeChallenge);
}

/** Generates a random PKCE code_verifier as stated */
private static String randomString(int nbBytes) {
SecureRandom random = new SecureRandom();
byte[] array = new byte[nbBytes];
random.nextBytes(array);
return Base64.getUrlEncoder().withoutPadding().encodeToString(array);
}

/** Creates an SHA-256 challenge from a code verifier */
private static String createCodeChallenge(String codeVerifier) {
try {
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(
MessageDigest.getInstance("SHA-256").digest(codeVerifier.getBytes(US_ASCII)));
} catch (NoSuchAlgorithmException e) {
throw new OidcClientException(e.getMessage(), e);
}
}
}
6 changes: 6 additions & 0 deletions src/main/java/io/okdp/spark/authc/model/PersistedToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.okdp.spark.authc.utils.JsonUtils;
import java.time.Instant;
import java.util.Date;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -51,4 +52,9 @@ public class PersistedToken {
public boolean isExpired() {
return Instant.now().isAfter(expiresAt.toInstant());
}

/** Convert this object to json */
public String toJson() {
return JsonUtils.toJson(this);
}
}
Loading