Skip to content

Commit

Permalink
Rewrite to own user db
Browse files Browse the repository at this point in the history
Token endpoint replaced with more generic user resource which also allows deleting oauth2proxy users (because the original user deletion API has their 2 internal sources hardcoded and rejects all others).

The realm also handles username + password for programmatic access now. It makes use of an hashed api token column in the new dedicated user db.

IDP group to nexus role mapping works without the name prefix. There's a dedicated db that collects the groups of all users logging in. Then they can be used with the original external role mapping mechanism of nexus.
  • Loading branch information
tumbl3w33d committed May 29, 2024
1 parent 177e7d2 commit c7c6638
Show file tree
Hide file tree
Showing 24 changed files with 1,948 additions and 246 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ frontend you-name-it
# circumvent oauth2 proxy for programmatic access
acl is_basic_auth hdr_beg(Authorization) -i basic
use_backend nexus if is_basic_auth
# clients often send a HEAD request without Authorization header first
# and this must reach nexus directly, else the OAuth2 dance starts
acl is_head method HEAD
use_backend nexus if is_basic_auth OR is_head
default_backend oauth2-proxy
Expand Down
9 changes: 6 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,28 +91,31 @@
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.valfirst</groupId>
<artifactId>slf4j-test</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>

<!-- UI plugin part -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function OAuth2ProxyApiTokenComponent() {
React.useEffect(() => {
(async () => {
try {
const reponse = await Axios.post("/service/rest/oauth2-proxy-api-token/reset-token");
const reponse = await Axios.post("/service/rest/oauth2-proxy/user/reset-token");
setToken(reponse.data);
} catch (e) {
setError(true);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@
import javax.inject.Inject;
import javax.inject.Named;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonatype.nexus.logging.task.TaskLogging;
import org.sonatype.nexus.orient.DatabaseInstance;
import org.sonatype.nexus.scheduling.Cancelable;
import org.sonatype.nexus.scheduling.TaskSupport;
import org.sonatype.nexus.security.user.UserManager;
import org.sonatype.nexus.security.user.UserNotFoundException;

import com.github.tumbl3w33d.users.OAuth2ProxyUserManager;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;
Expand All @@ -34,30 +33,26 @@ public class OAuth2ProxyApiTokenInvalidateTask
extends TaskSupport
implements Cancelable {

private final Logger logger = LoggerFactory.getLogger(OAuth2ProxyApiTokenInvalidateTask.class.getName());

private final DatabaseInstance databaseInstance;
private final UserManager nexusAuthenticatingRealm;

private final OAuth2ProxyUserManager userManager;

@Inject
public OAuth2ProxyApiTokenInvalidateTask(@Named(OAuth2ProxyDatabase.NAME) DatabaseInstance databaseInstance,
final List<UserManager> userManagers) {
final List<UserManager> userManagers, final OAuth2ProxyUserManager userManager) {

this.databaseInstance = databaseInstance;

this.nexusAuthenticatingRealm = userManagers.stream()
.filter(um -> um.getAuthenticationRealmName() == "NexusAuthenticatingRealm")
.findFirst().get();
this.userManager = userManager;

OAuth2ProxyRealm.ensureUserLoginTimestampSchema(databaseInstance, log);
}

private void resetPassword(String userId) {
private void resetApiToken(String userId) {
try {
nexusAuthenticatingRealm.changePassword(userId, generateSecureRandomString(32));
logger.debug("Password reset for user {} succeeded", userId);
userManager.changePassword(userId, generateSecureRandomString(32));
log.debug("API token reset for user {} succeeded", userId);
} catch (UserNotFoundException e) {
logger.error("Unable to reset password of user {} - {}", userId, e);
log.error("Unable to reset API token of user {} - {}", userId, e);
}
}

Expand All @@ -70,7 +65,7 @@ protected Void execute() throws Exception {
"select from " + CLASS_USER_LOGIN));

if (userLogins.isEmpty()) {
logger.debug("Nothing to do");
log.debug("Nothing to do");
} else {
for (ODocument userLogin : userLogins) {
String userId = userLogin.field(FIELD_USER_ID);
Expand All @@ -79,34 +74,32 @@ protected Void execute() throws Exception {
Instant lastLoginInstant = lastLoginDate.toInstant();
Instant nowInstant = Instant.now();

logger.debug("Last known login for {} was {}", userId,
log.debug("Last known login for {} was {}", userId,
formatDateString(lastLoginDate));

long timePassed = ChronoUnit.DAYS.between(lastLoginInstant, nowInstant);

int configuredDuration = getConfiguration()
.getInteger(OAuth2ProxyApiTokenInvalidateTaskDescriptor.CONFIG_EXPIRY, 1);

logger.debug("Time passed since login: {} - configured maximum: {}", timePassed,
log.debug("Time passed since login: {} - configured maximum: {}", timePassed,
configuredDuration);

if (timePassed >= configuredDuration) {
resetPassword(userId);
logger.info(
"Reset api token of user {} because they did not login via OAuth2 Proxy for a while",
userId);
resetApiToken(userId);
log.info("Reset api token of user {} because they did not show up for a while", userId);
}
}

}
} catch (Exception e) {
logger.error("Failed to retrieve login timestamps - {}", e);
log.error("Failed to retrieve login timestamps - {}", e);
}
return null;
}

@Override
public String getMessage() {
return "Invalidate OAuth2 Proxy API tokens of users who did not log in for a while";
return "Invalidate OAuth2 Proxy API tokens of users who did not show up for a while";
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/github/tumbl3w33d/OAuth2ProxyFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,10 @@ protected boolean onAccessDenied(ServletRequest request, ServletResponse respons

return false;
}

/* Only overriding to be able to mock it */
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
return super.executeLogin(request, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public Object getPrincipal() {

@Override
public Object getCredentials() {
logger.warn("there are no credentials for oauth2 proxy authentication - returning null");
logger.trace("there are no credentials for oauth2 proxy authentication - returning null");
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ public class OAuth2ProxyHeaderAuthTokenFactory extends HttpHeaderAuthenticationT

private final Logger logger = LoggerFactory.getLogger(OAuth2ProxyHeaderAuthTokenFactory.class);

private static final String X_FORWARDED_USER = "X-Forwarded-User";
private static final String X_FORWARDED_PREFERRED_USERNAME = "X-Forwarded-Preferred-Username";
private static final String X_FORWARDED_EMAIL = "X-Forwarded-Email";
private static final String X_FORWARDED_ACCESS_TOKEN = "X-Forwarded-Access-Token";
private static final String X_FORWARDED_GROUPS = "X-Forwarded-Groups";
static final String X_FORWARDED_USER = "X-Forwarded-User";
static final String X_FORWARDED_PREFERRED_USERNAME = "X-Forwarded-Preferred-Username";
static final String X_FORWARDED_EMAIL = "X-Forwarded-Email";
static final String X_FORWARDED_ACCESS_TOKEN = "X-Forwarded-Access-Token";
static final String X_FORWARDED_GROUPS = "X-Forwarded-Groups";

static final List<String> OAUTH2_PROXY_HEADERS = Collections
.unmodifiableList(Arrays.asList(X_FORWARDED_USER, X_FORWARDED_PREFERRED_USERNAME,
Expand All @@ -47,7 +47,7 @@ public AuthenticationToken createToken(ServletRequest request, ServletResponse r
} else {
if (xForwardedUserHeader == null || xForwardedEmailHeader == null
|| xForwardedPrefUsernameHeader == null) {
logger.warn("required OAuth2 proxy headers incomplete - {}: {} - {}: {} - {}: {}",
logger.debug("required OAuth2 proxy headers incomplete - {}: {} - {}: {} - {}: {}",
X_FORWARDED_USER, xForwardedUserHeader,
X_FORWARDED_EMAIL, xForwardedEmailHeader,
X_FORWARDED_PREFERRED_USERNAME, xForwardedPrefUsernameHeader);
Expand Down
Loading

0 comments on commit c7c6638

Please sign in to comment.