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

feat: make OAuth2 configuration optional #1057

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
34 changes: 30 additions & 4 deletions server/src/main/java/org/eclipse/openvsx/UserAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.ModelAndView;

import java.net.URI;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -66,14 +66,30 @@ public UserAPI(
this.storageUtil = storageUtil;
}

@GetMapping(
path = "/can-login",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Boolean> canLogin() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic())
.body(users.canLogin());
}

/**
* Redirect to GitHub Oauth2 login as default login provider.
*/
@GetMapping(
path = "/login"
)
public ModelAndView login(ModelMap model) {
return new ModelAndView("redirect:/oauth2/authorization/github", model);
public ResponseEntity<Void> login(ModelMap model) {
if(users.canLogin()) {
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(UrlUtil.createApiUrl(UrlUtil.getBaseUrl(), "oauth2", "authorization", "github")))
.build();
} else {
return ResponseEntity.notFound().build();
}
}

/**
Expand All @@ -84,7 +100,7 @@ public ModelAndView login(ModelMap model) {
produces = MediaType.APPLICATION_JSON_VALUE
)
public ErrorJson getAuthError(HttpServletRequest request) {
var authException = request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
var authException = users.canLogin() ? request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) : null;
if (!(authException instanceof AuthenticationException))
throw new ResponseStatusException(HttpStatus.NOT_FOUND);

Expand Down Expand Up @@ -241,6 +257,11 @@ public List<NamespaceJson> getOwnNamespaces() {
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<ResultJson> updateNamespaceDetails(@RequestBody NamespaceDetailsJson details) {
var user = users.findLoggedInUser();
if (user == null) {
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
}

try {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic())
Expand All @@ -262,6 +283,11 @@ public ResponseEntity<ResultJson> updateNamespaceDetailsLogo(
@PathVariable String namespace,
@RequestParam MultipartFile file
) {
var user = users.findLoggedInUser();
if (user == null) {
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
}

try {
return ResponseEntity.ok()
.body(users.updateNamespaceDetailsLogo(namespace, file));
Expand Down
15 changes: 14 additions & 1 deletion server/src/main/java/org/eclipse/openvsx/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
import org.eclipse.openvsx.security.IdPrincipal;
import org.eclipse.openvsx.storage.StorageUtilService;
import org.eclipse.openvsx.util.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -53,22 +55,29 @@ public class UserService {
private final StorageUtilService storageUtil;
private final CacheService cache;
private final ExtensionValidator validator;
private final ClientRegistrationRepository clientRegistrationRepository;

public UserService(
EntityManager entityManager,
RepositoryService repositories,
StorageUtilService storageUtil,
CacheService cache,
ExtensionValidator validator
ExtensionValidator validator,
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository
) {
this.entityManager = entityManager;
this.repositories = repositories;
this.storageUtil = storageUtil;
this.cache = cache;
this.validator = validator;
this.clientRegistrationRepository = clientRegistrationRepository;
}

public UserData findLoggedInUser() {
if(!canLogin()) {
return null;
}

var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
if (authentication.getPrincipal() instanceof IdPrincipal) {
Expand Down Expand Up @@ -315,4 +324,8 @@ public ResultJson deleteAccessToken(UserData user, long id) {
token.setActive(false);
return ResultJson.success("Deleted access token for user " + user.getLoginName() + ".");
}

public boolean canLogin() {
return clientRegistrationRepository != null && clientRegistrationRepository.findByRegistrationId("github") != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ public IdPrincipal loadUser(OAuth2UserRequest userRequest) {
}
}

public boolean canLogin() {
return users.canLogin();
}

private IdPrincipal loadGitHubUser(OAuth2UserRequest userRequest) {
var authUser = delegate.loadUser(userRequest);
String loginName = authUser.getAttribute("login");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
package org.eclipse.openvsx.security;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
Expand All @@ -30,12 +32,18 @@ public class SecurityConfig {
@Value("${ovsx.webui.frontendRoutes:/extension/**,/namespace/**,/user-settings/**,/admin-dashboard/**}")
String[] frontendRoutes;

private final ClientRegistrationRepository clientRegistrationRepository;

@Autowired
public SecurityConfig(@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, OAuth2UserServices userServices) throws Exception {
var redirectUrl = StringUtils.isEmpty(webuiUrl) ? "/" : webuiUrl;
return http.authorizeHttpRequests(
var filterChain = http.authorizeHttpRequests(
registry -> registry
.requestMatchers(antMatchers("/*", "/login/**", "/oauth2/**", "/user", "/user/auth-error", "/logout", "/actuator/health/**", "/actuator/metrics", "/actuator/metrics/**", "/actuator/prometheus", "/v3/api-docs/**", "/swagger-resources/**", "/swagger-ui/**", "/webjars/**"))
.requestMatchers(antMatchers("/*", "/login/**", "/oauth2/**", "/can-login", "/user", "/user/auth-error", "/logout", "/actuator/health/**", "/actuator/metrics", "/actuator/metrics/**", "/actuator/prometheus", "/v3/api-docs/**", "/swagger-resources/**", "/swagger-ui/**", "/webjars/**"))
.permitAll()
.requestMatchers(antMatchers("/api/*/*/review", "/api/*/*/review/delete", "/api/user/publish", "/api/user/namespace/create"))
.authenticated()
Expand All @@ -52,15 +60,20 @@ public SecurityFilterChain filterChain(HttpSecurity http, OAuth2UserServices use
.csrf(configurer -> {
configurer.ignoringRequestMatchers(antMatchers("/api/-/publish", "/api/-/namespace/create", "/api/-/query", "/vscode/**"));
})
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(new Http403ForbiddenEntryPoint()))
.oauth2Login(configurer -> {
configurer.defaultSuccessUrl(redirectUrl);
configurer.successHandler(new CustomAuthenticationSuccessHandler(redirectUrl));
configurer.failureUrl(redirectUrl + "?auth-error");
configurer.userInfoEndpoint(customizer -> customizer.oidcUserService(userServices.getOidc()).userService(userServices.getOauth2()));
})
.logout(configurer -> configurer.logoutSuccessUrl(redirectUrl))
.build();
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(new Http403ForbiddenEntryPoint()));

if(userServices.canLogin()) {
var redirectUrl = StringUtils.isEmpty(webuiUrl) ? "/" : webuiUrl;
filterChain.oauth2Login(configurer -> {
configurer.defaultSuccessUrl(redirectUrl);
configurer.successHandler(new CustomAuthenticationSuccessHandler(redirectUrl));
configurer.failureUrl(redirectUrl + "?auth-error");
configurer.userInfoEndpoint(customizer -> customizer.oidcUserService(userServices.getOidc()).userService(userServices.getOauth2()));
})
.logout(configurer -> configurer.logoutSuccessUrl(redirectUrl));
}

return filterChain.build();
}

private RequestMatcher[] antMatchers(String... patterns)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.beans.factory.annotation.Autowired;

import java.time.Instant;
import java.util.Arrays;
Expand All @@ -44,16 +45,20 @@ public class TokenService {
public TokenService(
TransactionTemplate transactions,
EntityManager entityManager,
ClientRegistrationRepository clientRegistrationRepository
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository
) {
this.transactions = transactions;
this.entityManager = entityManager;
this.clientRegistrationRepository = clientRegistrationRepository;
}

private boolean isEnabled() {
return clientRegistrationRepository != null;
}

public AuthToken updateTokens(long userId, String registrationId, OAuth2AccessToken accessToken,
OAuth2RefreshToken refreshToken) {
var userData = entityManager.find(UserData.class, userId);
var userData = isEnabled() ? entityManager.find(UserData.class, userId) : null;
if (userData == null) {
return null;
}
Expand Down Expand Up @@ -119,6 +124,10 @@ private AuthToken updateEclipseToken(UserData userData, AuthToken token) {
}

public AuthToken getActiveToken(UserData userData, String registrationId) {
if(!isEnabled()) {
return null;
}

switch (registrationId) {
case "github": {
return userData.getGithubToken();
Expand Down Expand Up @@ -148,7 +157,7 @@ private boolean isExpired(Instant instant) {
return instant != null && Instant.now().isAfter(instant);
}

protected Pair<OAuth2AccessToken, OAuth2RefreshToken> refreshEclipseToken(AuthToken token) {
private Pair<OAuth2AccessToken, OAuth2RefreshToken> refreshEclipseToken(AuthToken token) {
if(token.refreshToken() == null || isExpired(token.refreshExpiresAt())) {
return null;
}
Expand Down
2 changes: 2 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/web/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public void addCorsMappings(CorsRegistry registry) {
.allowedOrigins(webuiUrl)
.allowCredentials(true);
}
registry.addMapping("/can-login")
.allowedOrigins(webuiUrl);
registry.addMapping("/documents/**")
.allowedOrigins("*");
registry.addMapping("/api/**")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -915,9 +915,10 @@ UserService userService(
RepositoryService repositories,
StorageUtilService storageUtil,
CacheService cache,
ExtensionValidator validator
ExtensionValidator validator,
ClientRegistrationRepository clientRegistrationRepository
) {
return new UserService(entityManager, repositories, storageUtil, cache, validator);
return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository);
}

@Bean
Expand Down
1 change: 1 addition & 0 deletions webui/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface MainContext {
handleError: (err: Error | Partial<ErrorResponse>) => void;
user?: UserData;
updateUser: () => void;
canLogin: boolean;
}

// We don't include `undefined` as context value to avoid checking the value in all components
Expand Down
Loading
Loading