Skip to content

Commit

Permalink
fix: Allow ignoring refresh token storage in the cookie #40
Browse files Browse the repository at this point in the history
  • Loading branch information
idirze committed Feb 3, 2025
1 parent edaf38c commit a797066
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 26 deletions.
33 changes: 17 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,23 @@ Once done, save the `client_id` and `client_secret` (Confidential clients only)

The filter relies on the spark [spark.ui.filters](https://spark.apache.org/docs/latest/configuration.html) configuration property.

| Property | Equivalent env variable | Default | Description |
|:---------------------------|-------------------------------|:-------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `issuer-uri` | `AUTH_ISSUER_URI` | - | OIDC Provider issuer URL</br>This is used to discover OIDC endpoints |
| `client-id` | `AUTH_CLIENT_ID` | - | The Oauth2/OIDC client Id |
| `client-secret` | `AUTH_CLIENT_SECRET` | - | The Oauth2/OIDC client secret </br> * Mandatory for Confidential Clients. </br> * Optional for Public clients. </br> |
| `redirect-uri` | `AUTH_REDIRECT_URI` | - | Spark UI/History home page</br>ex.: https://spark-history.example.com/home |
| `scope` | `AUTH_SCOPE` | - | The scope(s) requested by the Authorization Request.</br>Example: `openid+profile+email+roles+offline_access` |
| `use-pkce` | `AUTH_USE_PKCE` | auto | * `true`: Force the usage of PKCE (The OIDC provider should support it). </br>* `false`: Disable the usage of PKCE for confidential clients. </br> * `auto`: Detect if OIDC provider supports PKCE and use it, otherwise use Authorization Code standard flow. |
| `cookie-max-age-minutes` | `AUTH_COOKE_MAX_AGE_MINUTES` | 12 * 60 | The maximum spark-cookie cookie duration in minutes |
| `cookie-cipher-secret-key` | `AUTH_COOKIE_ENCRYPTION_KEY` | - | Cookie encryption key</br> Can be generated using: `openssl enc -aes-128-cbc -k <PASS PHRASE> -P -md sha1 -pbkdf2` |
| `cookie-is-secure` | `AUTH_COOKE_IS_SECURE` | true | When enabled, the cookie is transmitted over a secure connection only (HTTPS).</br> Disable the option if your run with a non secure connection (HTTP) |
| `user-id` | `AUTH_USER_ID` | email | * `email`: set the id seen by spark acls as the email filled in the access token. </br> * `sub`: set the id seen by spark acls as the sub filled in the access. </br> * `google`: set the id to the sub sent by google but remove the prefix 'account.google.com:'. |
| `jwt-header` | `JWT_HEADER` | jwt_token | Header that may contain the JWT Token that will be used for authentication. If not present, it will fall back with the default autentication workflow with a redirection on the login page. |
| `jwt-header-signing-alg` | `JWT_HEADER_SIGNING_ALG` | RS256, ES256 | Signature algorithm used to verify the JWT Token provided. |
| `jwt-header-issuer` | `JWT_HEADER_ISSUER` | issuer-uri from well known configuration | Issuer if different from the default issuer uri retrieved from the well known configuration fetched with 'issuer-uri' parameter. |
| `jwt-header-jwks-uri` | `JWT_HEADER_JWKS_URI` | jwks uri from well known configuration | JWKS URI used to retrieve the key needed to verify the JWT token signature. By default will use the JWKS URI filled in the well known configuration fetched with 'issuer-uri' parameter. |
| Property | Equivalent env variable | Default | Description |
|:---------------------------|-------------------------------|:----------------------------------------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `issuer-uri` | `AUTH_ISSUER_URI` | - | OIDC Provider issuer URL</br>This is used to discover OIDC endpoints |
| `client-id` | `AUTH_CLIENT_ID` | - | The Oauth2/OIDC client Id |
| `client-secret` | `AUTH_CLIENT_SECRET` | - | The Oauth2/OIDC client secret </br> * Mandatory for Confidential Clients. </br> * Optional for Public clients. </br> |
| `redirect-uri` | `AUTH_REDIRECT_URI` | - | Spark UI/History home page</br>ex.: https://spark-history.example.com/home |
| `scope` | `AUTH_SCOPE` | - | The scope(s) requested by the Authorization Request.</br>Example: `openid+profile+email+roles+offline_access` |
| `use-pkce` | `AUTH_USE_PKCE` | auto | * `true`: Force the usage of PKCE (The OIDC provider should support it). </br>* `false`: Disable the usage of PKCE for confidential clients. </br> * `auto`: Detect if OIDC provider supports PKCE and use it, otherwise use Authorization Code standard flow. |
| `cookie-max-age-minutes` | `AUTH_COOKE_MAX_AGE_MINUTES` | 12 * 60 | The maximum spark-cookie cookie duration in minutes |
| `cookie-cipher-secret-key` | `AUTH_COOKIE_ENCRYPTION_KEY` | - | Cookie encryption key</br> Can be generated using: `openssl enc -aes-128-cbc -k <PASS PHRASE> -P -md sha1 -pbkdf2` |
| `cookie-is-secure` | `AUTH_COOKE_IS_SECURE` | true | When enabled, the cookie is transmitted over a secure connection only (HTTPS).</br> Disable the option if your run with a non secure connection (HTTP) |
| `user-id` | `AUTH_USER_ID` | email | * `email`: set the id seen by spark acls as the email filled in the access token. </br> * `sub`: set the id seen by spark acls as the sub filled in the access. </br> * `google`: set the id to the sub sent by google but remove the prefix 'account.google.com:'. |
| `jwt-header` | `JWT_HEADER` | jwt_token | Header that may contain the JWT Token that will be used for authentication. If not present, it will fall back with the default autentication workflow with a redirection on the login page. |
| `jwt-header-signing-alg` | `JWT_HEADER_SIGNING_ALG` | RS256, ES256 | Signature algorithm used to verify the JWT Token provided. |
| `jwt-header-issuer` | `JWT_HEADER_ISSUER` | issuer-uri from well known configuration | Issuer if different from the default issuer uri retrieved from the well known configuration fetched with 'issuer-uri' parameter. |
| `jwt-header-jwks-uri` | `JWT_HEADER_JWKS_URI` | jwks uri from well known configuration | JWKS URI used to retrieve the key needed to verify the JWT token signature. By default will use the JWKS URI filled in the well known configuration fetched with 'issuer-uri' parameter. |
| `ignore-refresh-token` | `IGNORE_REFRESH_TOKEN` | false | * `true`: Ignore refresh token storage in the cookie (Prevent exceeding the cookie size limit). </br>* `false`: Store the refresh token in the cookie. |

> [!NOTE]
> 1. `issuer-uri` property or `AUTH_ISSUER_URI` env variable
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<groupId>io.okdp</groupId>
<artifactId>okdp-spark-auth-filter</artifactId>
<version>1.3.1</version>
<version>1.3.2-snapshot</version>

<name>OIDC authentication filter for Apache spark</name>
<description>OIDC authentication filter for Apache spark web UIs (Spark app and History Web UIs)
Expand Down
17 changes: 13 additions & 4 deletions src/main/java/io/okdp/spark/authc/OidcAuthFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ public void init(FilterConfig filterConfig) throws ServletException {
ofNullable(filterConfig.getInitParameter(AUTH_SCOPE))
.orElse(System.getenv("AUTH_SCOPE")),
AUTH_SCOPE);
boolean ignoreRefreshToken =
Boolean.parseBoolean(
ofNullable(filterConfig.getInitParameter(IGNORE_REFRESH_TOKEN))
.orElse(ofNullable(System.getenv("IGNORE_REFRESH_TOKEN")).orElse("false"))
.toLowerCase());
String encryptionKey =
PreconditionsUtils.checkNotNull(
ofNullable(filterConfig.getInitParameter(AUTH_COOKIE_ENCRYPTION_KEY))
Expand Down Expand Up @@ -147,11 +152,13 @@ public void init(FilterConfig filterConfig) throws ServletException {
.or(() -> ofNullable(System.getenv("JWT_HEADER_JWKS_URI")));

log.info(
"Initializing OIDC Auth filter ({}: <{}>, {}: <{}>) ...",
"Initializing OIDC Auth filter ({}: <{}>, {}: <{}>, {}: <{}>) ...",
AUTH_ISSUER_URI,
issuerUri,
AUTH_CLIENT_ID,
clientId);
clientId,
IGNORE_REFRESH_TOKEN,
ignoreRefreshToken);

ofNullable(clientSecret)
.ifPresentOrElse(
Expand All @@ -174,6 +181,7 @@ public void init(FilterConfig filterConfig) throws ServletException {
.scope(scope)
.usePKCE(usePKCE)
.identityProvider(IdentityProviderFactory.from(TokenUtils.capitalize(idProvider)))
.ignoreRefreshToken(ignoreRefreshToken)
.wellKnownConfiguration(
JsonUtils.loadJsonFromUrl(
format("%s%s", issuerUri, AUTH_ISSUER_WELL_KNOWN_CONFIGURATION),
Expand Down Expand Up @@ -225,7 +233,8 @@ public void init(FilterConfig filterConfig) throws ServletException {
domain(oidcConfig.redirectUri()),
isCookieSecure,
encryptionKey,
cookieMaxAgeMinutes * 60))
cookieMaxAgeMinutes * 60,
ignoreRefreshToken))
.configure();
try {
// Define the token's type allowed
Expand Down Expand Up @@ -305,7 +314,7 @@ public void doFilter(
e.getMessage()));
} else {
log.info(
"The user {} token was expired, removing cookie and attempt to re-authenticate ... ",
"The user {} token was expired and no refresh token found. Removing cookie and attempt to re-authenticate ... ",
persistedToken.userInfo().email());
}
PersistedToken pToken =
Expand Down
3 changes: 3 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 @@ -36,6 +36,9 @@ public interface Constants {
/** The oidc scope (ex.: openid+profile+email+groups+offline_access) */
String AUTH_SCOPE = "scope";

/** Allow ignoring refresh token after token exchange */
String IGNORE_REFRESH_TOKEN = "ignore-refresh-token";

/** OIDC standard well-known configuration endpoint */
String AUTH_ISSUER_WELL_KNOWN_CONFIGURATION = "/.well-known/openid-configuration";

Expand Down
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,6 +32,7 @@ public class OidcConfig {
private String redirectUri;
private String responseType;
private String scope;
private boolean ignoreRefreshToken;
private WellKnownConfiguration wellKnownConfiguration;
private String usePKCE;
private IdentityProvider identityProvider;
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/io/okdp/spark/authc/model/PersistedToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public boolean isExpired() {

@JsonIgnore
public boolean hasRefreshToken() {
return Strings.nullToEmpty(refreshToken).trim().isEmpty();
return !Strings.nullToEmpty(refreshToken).trim().isEmpty();
}

/** Convert this object to json */
Expand All @@ -74,4 +74,16 @@ public String toJson() {
public String id() {
return identityProvider.extractId(this.userInfo);
}

/**
* Ignore refresh token
*
* @param ignore: whether to ignore refresh token storage
*/
public PersistedToken ignoreRefreshToken(boolean ignore) {
if (ignore) {
this.refreshToken = "";
}
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class CookieSessionStore implements SessionStore {
@NonNull private Boolean isSecure;
@NonNull private String encryptionKey;
@NonNull private Integer cookieMaxAgeSeconds;
@NonNull Boolean ignoreRefreshToken;

/**
* Compress, encrypt and save the access token in a {@link Cookie}
Expand All @@ -66,6 +67,7 @@ public Cookie save(PersistedToken persistedToken) {
// Encrypt the content to prevent token corruption
String cookieValue =
ofNullable(persistedToken)
.map(token -> token.ignoreRefreshToken(ignoreRefreshToken))
.map(token -> persistedToken.toJson())
.map(tokenAsJson -> encryptToString(compressToString(tokenAsJson), encryptionKey))
.orElse("");
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/io/okdp/spark/authc/utils/JsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ public static <T> T loadJsonFromUrl(String url, Class<T> type) throws OidcClient
public static <T> T loadJsonFromString(String json, Class<T> type) throws RuntimeException {
try {
return mapper.readValue(json, type);
} catch (JsonProcessingException e) {
throw new OidcClientException(format("Unable to load json data into the class %s", type), e);
} catch (IOException e) {
throw new OidcClientException(format("Unable to load json data into the class %s", type), e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ public void setUp() {
HttpAuthenticationUtils.domain(redirectUri),
HttpAuthenticationUtils.isSecure(redirectUri),
cookieEncryptionKey,
60))
60,
false))
.configure();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public void should_save_and_read_token_stored_in_cookie() {
String cookieDomain = "spark.okdp.local";
SessionStore sessionStore =
CookieSessionStore.of(
cookieName, cookieDomain, true, "E132A72E815F496FFC49B3EC876754F4", 60);
cookieName, cookieDomain, true, "E132A72E815F496FFC49B3EC876754F4", 60, false);

// When
PersistedToken originalPersistedToken =
Expand Down Expand Up @@ -138,4 +138,43 @@ public void should_save_and_read_token_stored_in_cookie() {
Instant.parse("2024-02-21T10:11:12.123Z").plusSeconds(accessToken.expiresIn() - 1));
assertThat(persistedToken.userInfo()).isEqualTo(TokenUtils.userInfo(accessToken.accessToken()));
}

@Test
public void should_save_and_read_token_stored_in_cookie_and_ignore_refresh_token() {
// Given
String cookieName = "spark";
String cookieDomain = "spark.okdp.local";
SessionStore sessionStore =
CookieSessionStore.of(
cookieName, cookieDomain, true, "E132A72E815F496FFC49B3EC876754F4", 60, true);

// When
PersistedToken originalPersistedToken =
PersistedToken.builder()
.userInfo(TokenUtils.userInfo(accessToken.accessToken()))
.refreshToken(accessToken.refreshToken())
.expiresIn(accessToken.expiresIn())
.expiresAt(
Date.from(
Instant.parse("2024-02-21T10:11:12.123Z").plusSeconds(accessToken.expiresIn())))
.identityProvider(new EmailIdentityProvider())
.build();
Cookie cookie = sessionStore.save(originalPersistedToken);
PersistedToken persistedToken = sessionStore.readToken(cookie.getValue());

// Then
assertThat(cookie.getName()).isEqualTo(cookieName);
assertThat(cookie.getDomain()).isEqualTo(cookieDomain);
assertThat(cookie.getMaxAge()).isEqualTo(60);
assertThat(cookie.isHttpOnly()).isEqualTo(true);
assertThat(cookie.getSecure()).isEqualTo(true);
assertThat(cookie.getPath()).isEqualTo("/;SameSite=Strict;");
assertThat(persistedToken.refreshToken()).isEmpty();
assertThat(persistedToken.hasRefreshToken()).isFalse();
assertThat(persistedToken.expiresIn()).isEqualTo(accessToken.expiresIn());
assertThat(persistedToken.expiresAt())
.isAfter(
Instant.parse("2024-02-21T10:11:12.123Z").plusSeconds(accessToken.expiresIn() - 1));
assertThat(persistedToken.userInfo()).isEqualTo(TokenUtils.userInfo(accessToken.accessToken()));
}
}

0 comments on commit a797066

Please sign in to comment.