Skip to content

Commit

Permalink
feat: Add support for Sub claim (default claim is email). Sub or emai…
Browse files Browse the repository at this point in the history
…l could be used as id for acls
  • Loading branch information
lioneloh authored and idirze committed Apr 11, 2024
1 parent 6720947 commit 2d1283d
Show file tree
Hide file tree
Showing 17 changed files with 255 additions and 51 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ The filter relies on the spark [spark.ui.filters](https://spark.apache.org/docs/
| `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 token. |

> [!NOTE]
> 1. `issuer-uri` property or `AUTH_ISSUER_URI` env variable
Expand Down Expand Up @@ -166,6 +167,7 @@ spark.io.okdp.spark.authc.OidcAuthFilter.param.scope=<scope>
spark.io.okdp.spark.authc.OidcAuthFilter.param.cookie-max-age-minutes=480
spark.io.okdp.spark.authc.OidcAuthFilter.param.cookie-cipher-secret-key=<cookie-cipher-secret-key>
spark.io.okdp.spark.authc.OidcAuthFilter.param.cookie-is-secure=<true|false>
spark.io.okdp.spark.authc.OidcAuthFilter.param.user-id=<sub|email>
```
Or during the job submission like the following:
Expand All @@ -180,6 +182,7 @@ spark-submit --conf spark.ui.filters=io.okdp.spark.authc.OidcAuthFilter \
--conf spark.io.okdp.spark.authc.OidcAuthFilter.param.cookie-max-age-minutes=480 \
--conf spark.io.okdp.spark.authc.OidcAuthFilter.param.cookie-cipher-secret-key=<cookie-cipher-secret-key> \
--conf spark.io.okdp.spark.authc.OidcAuthFilter.param.cookie-is-secure=<true|false> \
--conf spark.io.okdp.spark.authc.OidcAuthFilter.param.user-id=<sub|email> \
--class ...
```
Expand Down
38 changes: 23 additions & 15 deletions src/main/java/io/okdp/spark/authc/OidcAuthFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
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;
import static java.lang.String.format;
import static java.util.Optional.ofNullable;

Expand All @@ -32,13 +31,14 @@
import io.okdp.spark.authc.exception.AuthenticationException;
import io.okdp.spark.authc.model.AccessToken;
import io.okdp.spark.authc.model.PersistedToken;
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.IdentityProviderFactory;
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.TokenUtils;
import io.okdp.spark.authc.utils.exception.Try;
import io.okdp.spark.authz.OidcGroupMappingServiceProvider;
import java.io.IOException;
Expand Down Expand Up @@ -105,6 +105,9 @@ public void init(FilterConfig filterConfig) {
String usePKCE =
ofNullable(filterConfig.getInitParameter(AUTH_USE_PKCE))
.orElse(ofNullable(System.getenv("AUTH_USE_PKCE")).orElse("auto"));
String idProvider =
ofNullable(filterConfig.getInitParameter(AUTH_USER_ID))
.orElse(ofNullable(System.getenv("AUTH_USER_ID")).orElse("Email"));

log.info(
"Initializing OIDC Auth filter ({}: <{}>, {}: <{}>) ...",
Expand Down Expand Up @@ -133,6 +136,7 @@ public void init(FilterConfig filterConfig) {
.responseType("code")
.scope(scope)
.usePKCE(usePKCE)
.identityProvider(IdentityProviderFactory.from(TokenUtils.capitalize(idProvider)))
.wellKnownConfiguration(
JsonUtils.loadJsonFromUrl(
format("%s%s", issuerUri, AUTH_ISSUER_WELL_KNOWN_CONFIGURATION),
Expand Down Expand Up @@ -217,15 +221,16 @@ public void doFilter(
log.info(
"Unable to renew access token from refresh token, removing cookie and retrying ....., cause: {}",
e.getMessage()));
Cookie cookie = authProvider.httpSecurityConfig().sessionStore().save(accessToken);
PersistedToken pToken = authProvider.httpSecurityConfig().toPersistedToken(accessToken);
Cookie cookie = authProvider.httpSecurityConfig().sessionStore().save(pToken);
((HttpServletResponse) servletResponse).addCookie(cookie);
}
// Add the user and groups in the user/group mappings authorization cache
OidcGroupMappingServiceProvider.addUserAndGroups(
persistedToken.userInfo().email(), persistedToken.userInfo().getGroupsAndRoles());
persistedToken.id(), persistedToken.userInfo().getGroupsAndRoles());
filterChain.doFilter(
new PrincipalHttpServletRequestWrapper(
(HttpServletRequest) servletRequest, persistedToken.userInfo().email()),
(HttpServletRequest) servletRequest, persistedToken.id()),
servletResponse);
return;
}
Expand All @@ -247,30 +252,33 @@ public void doFilter(
AccessToken accessToken =
Try.of(() -> authProvider.requestAccessToken(servletRequest, servletResponse))
.onException(e -> sendError(servletResponse, e.getHttpStatusCode(), e.getMessage()));
UserInfo userInfo = userInfo(accessToken.accessToken());
PersistedToken persistedToken =
authProvider.httpSecurityConfig().toPersistedToken(accessToken);
// UserInfo userInfo = authProvider.httpSecurityConfig().userInfo(accessToken.accessToken());
log.info(
"Successfully authenticated user ({}): {} (roles: {}, groups: {})",
userInfo.name(),
userInfo.email(),
userInfo.roles(),
userInfo.groups());
"Successfully authenticated user ({}): email {} sub {} (roles: {}, groups: {})",
persistedToken.userInfo().name(),
persistedToken.userInfo().email(),
persistedToken.userInfo().sub(),
persistedToken.userInfo().roles(),
persistedToken.userInfo().groups());

Try.of(
() ->
ofNullable(userInfo.email())
ofNullable(persistedToken.id())
.orElseThrow(
() ->
new AuthenticationException(
HttpStatus.SC_UNAUTHORIZED,
"Your oidc provider returned an empty user email and may have expired your oidc session! "
"Your oidc provider returned an empty user id and may have expired your oidc session! "
+ "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().sessionStore().save(accessToken);
Cookie cookie = authProvider.httpSecurityConfig().sessionStore().save(persistedToken);
((HttpServletResponse) servletResponse).addCookie(cookie);
// Add the user and groups in the user/group mappings authorization cache
OidcGroupMappingServiceProvider.addUserAndGroups(
userInfo.email(), userInfo.getGroupsAndRoles());
persistedToken.id(), persistedToken.userInfo().getGroupsAndRoles());
// Redirect the user from the browser (client) side into spark/history UI home page (i.e.
// remove the authz 'code' from the browser)
servletResponse
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 @@ -63,6 +63,9 @@ public interface Constants {
/** Use PKCE (true|false|auto) */
String AUTH_USE_PKCE = "use-pkce";

/** The user id wich will be extracted from the access token (EMAIL|SUB) */
String AUTH_USER_ID = "user-id";

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

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/io/okdp/spark/authc/config/HttpSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@

package io.okdp.spark.authc.config;

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

import io.okdp.spark.authc.model.AccessToken;
import io.okdp.spark.authc.model.PersistedToken;
import io.okdp.spark.authc.provider.AuthProvider;
import io.okdp.spark.authc.provider.SessionStore;
import io.okdp.spark.authc.provider.impl.DefaultAuthorizationCodeAuthProvider;
import io.okdp.spark.authc.provider.impl.PKCEAuthorizationCodeAuthProvider;
import io.okdp.spark.authc.utils.TokenUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -62,6 +67,16 @@ public HttpSecurityConfig sessionStore(SessionStore sessionStore) {
return this;
}

public PersistedToken toPersistedToken(AccessToken token) {
return PersistedToken.builder()
.userInfo(TokenUtils.userInfo(token.accessToken()))
.refreshToken(token.refreshToken())
.expiresIn(token.expiresIn())
.expiresAt(from(now().plusSeconds(token.expiresIn())))
.identityProvider(oidcConfig().identityProvider())
.build();
}

/** Configure the auth provider */
public AuthProvider configure() {
switch (oidcConfig.usePKCE().toLowerCase()) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/okdp/spark/authc/config/OidcConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.okdp.spark.authc.config;

import io.okdp.spark.authc.model.WellKnownConfiguration;
import io.okdp.spark.authc.provider.IdentityProvider;
import lombok.Builder;
import lombok.Getter;
import lombok.experimental.Accessors;
Expand All @@ -33,4 +34,5 @@ public class OidcConfig {
private String scope;
private WellKnownConfiguration wellKnownConfiguration;
private String usePKCE;
private IdentityProvider identityProvider;
}
13 changes: 0 additions & 13 deletions src/main/java/io/okdp/spark/authc/model/AccessToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@

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 @@ -47,13 +43,4 @@ 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();
}
}
11 changes: 11 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 @@ -17,8 +17,10 @@
package io.okdp.spark.authc.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.okdp.spark.authc.provider.IdentityProvider;
import io.okdp.spark.authc.utils.JsonUtils;
import java.time.Instant;
import java.util.Date;
Expand All @@ -36,6 +38,9 @@
@NoArgsConstructor
public class PersistedToken {

@JsonProperty("identity_provider")
private IdentityProvider identityProvider;

@JsonProperty("access_token_payload")
private UserInfo userInfo;

Expand All @@ -49,6 +54,7 @@ public class PersistedToken {
@JsonProperty("refresh_token")
private String refreshToken;

@JsonIgnore
public boolean isExpired() {
return Instant.now().isAfter(expiresAt.toInstant());
}
Expand All @@ -57,4 +63,9 @@ public boolean isExpired() {
public String toJson() {
return JsonUtils.toJson(this);
}

/** Extract the id from UserInfo */
public String id() {
return identityProvider.extractId(this.userInfo);
}
}
5 changes: 5 additions & 0 deletions src/main/java/io/okdp/spark/authc/model/UserInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static java.util.Collections.emptyList;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
Expand All @@ -31,6 +32,9 @@
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserInfo {

@JsonProperty("sub")
private String sub;

@JsonProperty("name")
private String name;

Expand All @@ -48,6 +52,7 @@ public class UserInfo {
*
* @return the list of the groups or roles
*/
@JsonIgnore
public List<String> getGroupsAndRoles() {
return Stream.concat(groups.stream(), roles.stream()).collect(Collectors.toList());
}
Expand Down
45 changes: 45 additions & 0 deletions src/main/java/io/okdp/spark/authc/provider/IdentityProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.provider;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.okdp.spark.authc.model.UserInfo;
import io.okdp.spark.authc.provider.impl.EmailIdentityProvider;
import io.okdp.spark.authc.provider.impl.SubIdentityProvider;

@JsonTypeInfo(
include = JsonTypeInfo.As.PROPERTY,
use = JsonTypeInfo.Id.NAME,
property = "type",
defaultImpl = EmailIdentityProvider.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = EmailIdentityProvider.class, name = "email"),
@JsonSubTypes.Type(value = SubIdentityProvider.class, name = "sub")
})

/** Each concrete Identity provider should implement this interface */
public interface IdentityProvider {

/**
* Extract the id form the userInfo ({@link UserInfo})
*
* @param UserInfo the userInfo extracted from the user's access token
* @return the id as a ({@link String})
*/
String extractId(UserInfo userInfo);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.provider;

import io.okdp.spark.authc.exception.OidcClientException;
import java.lang.reflect.InvocationTargetException;

public class IdentityProviderFactory {
public static IdentityProvider from(String provider) throws OidcClientException {
try {
return (IdentityProvider)
Class.forName("io.okdp.spark.authc.provider.impl." + provider + "IdentityProvider")
.getDeclaredConstructor()
.newInstance();
} catch (InstantiationException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException
| NoSuchMethodException
| SecurityException
| ClassNotFoundException e) {
throw new OidcClientException("ID provider not found", e);
}
}
}
6 changes: 3 additions & 3 deletions src/main/java/io/okdp/spark/authc/provider/SessionStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@

package io.okdp.spark.authc.provider;

import io.okdp.spark.authc.model.AccessToken;
import io.okdp.spark.authc.model.AuthState;
import io.okdp.spark.authc.model.PersistedToken;

/** Each concrete token store should implement this interface */
public interface SessionStore {

/**
* Save the access token in a {@link T}
*
* @param accessToken the access token response from the oidc provider
* @param PersistedToken the persisted token issued from the response from the oidc provider
* @return {@link T} containing the saved access token
*/
<T> T save(AccessToken accessToken);
<T> T save(PersistedToken PersistedToken);

/**
* Save the PKCE state in a {@link T}
Expand Down
Loading

0 comments on commit 2d1283d

Please sign in to comment.