Skip to content

Commit

Permalink
Make authentication configurable
Browse files Browse the repository at this point in the history
- Replace the references to `OAuth2User` by `AuthUser`. This allows downstream extenders to more easily contribute alternative OAuth2 providers: If the expected data is stored in different attributes it will be possible to bridge it by implementing the proper `AuthUser`.
- Introduce a configuration to define attribute names to use when mapping attributes from an auth provider.
- Allow configuring the auth for arbitrary providers (other than github).
  • Loading branch information
amvanbaren committed Feb 20, 2025
1 parent 63d6478 commit 3d91eed
Show file tree
Hide file tree
Showing 15 changed files with 527 additions and 127 deletions.
123 changes: 123 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/OVSXConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/********************************************************************************
* Copyright (c) 2023 Ericsson and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
package org.eclipse.openvsx;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.Collections;
import java.util.Map;

/**
* TODO: use lombok to reduce boilerplate, it's very needed.
*/
@Configuration
@ConfigurationProperties(prefix = "ovsx")
public class OVSXConfig {

private OAuth2Config oauth2 = new OAuth2Config();

public OAuth2Config getOauth2() {
return oauth2;
}

public void setOauth2(OAuth2Config oauth2Config) {
this.oauth2 = oauth2Config;
}

public static class OAuth2Config {

/**
* The user authentication provider to use.
*/
private String provider = "github";

/**
* Configuration example:
* <pre><code>
*ovsx:
* oauth2:
* attribute-names:
* [provider-name]:
* avatar-url: string
* email: string
* full-name: string
* login-name: string
* provider-url: string
* </code></pre>
*/
private Map<String, AttributeNames> attributeNames = Collections.emptyMap();

public String getProvider() {
return provider;
}

public void setProvider(String provider) {
this.provider = provider;
}

public Map<String, AttributeNames> getAttributeNames() {
return attributeNames;
}

public void setAttributeNames(Map<String, AttributeNames> attributeNames) {
this.attributeNames = attributeNames;
}

public static class AttributeNames {

private String avatarUrl;
private String email;
private String fullName;
private String loginName;
private String providerUrl;

public String getAvatarUrl() {
return avatarUrl;
}

public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}

public String getLoginName() {
return loginName;
}

public void setLoginName(String loginName) {
this.loginName = loginName;
}

public String getProviderUrl() {
return providerUrl;
}

public void setProviderUrl(String providerUrl) {
this.providerUrl = providerUrl;
}
}
}
}
11 changes: 7 additions & 4 deletions server/src/main/java/org/eclipse/openvsx/UserAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,20 @@ public class UserAPI {
private final UserService users;
private final EclipseService eclipse;
private final StorageUtilService storageUtil;
private final OVSXConfig config;

public UserAPI(
RepositoryService repositories,
UserService users,
EclipseService eclipse,
StorageUtilService storageUtil
StorageUtilService storageUtil,
OVSXConfig config
) {
this.repositories = repositories;
this.users = users;
this.eclipse = eclipse;
this.storageUtil = storageUtil;
this.config = config;
}

@GetMapping(
Expand All @@ -85,7 +88,7 @@ public ResponseEntity<Boolean> canLogin() {
public ResponseEntity<Void> login(ModelMap model) {
if(users.canLogin()) {
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(UrlUtil.createApiUrl(UrlUtil.getBaseUrl(), "oauth2", "authorization", "github")))
.location(URI.create(UrlUtil.createApiUrl(UrlUtil.getBaseUrl(), "oauth2", "authorization", config.getOauth2().getProvider())))
.build();
} else {
return ResponseEntity.notFound().build();
Expand Down Expand Up @@ -313,7 +316,7 @@ public ResponseEntity<NamespaceMembershipListJson> getNamespaceMembers(@PathVari
membershipList.setNamespaceMemberships(memberships.stream().map(NamespaceMembership::toJson).toList());
return new ResponseEntity<>(membershipList, HttpStatus.OK);
} else {
return new ResponseEntity<>(NamespaceMembershipListJson.error("You don't have the permission to see this."), HttpStatus.FORBIDDEN);
return new ResponseEntity<>(NamespaceMembershipListJson.error("You don't have the permission to see this."), HttpStatus.FORBIDDEN);
}
}

Expand Down Expand Up @@ -366,4 +369,4 @@ public ResponseEntity<UserJson> signPublisherAgreement() {
}
}

}
}
34 changes: 17 additions & 17 deletions server/src/main/java/org/eclipse/openvsx/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@
import org.eclipse.openvsx.json.NamespaceDetailsJson;
import org.eclipse.openvsx.json.ResultJson;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.security.AuthUser;
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 Down Expand Up @@ -89,44 +89,44 @@ public UserData findLoggedInUser() {
}

@Transactional
public UserData registerNewUser(OAuth2User oauth2User) {
public UserData registerNewUser(AuthUser authUser) {
var user = new UserData();
user.setProvider("github");
user.setAuthId(oauth2User.getName());
user.setLoginName(oauth2User.getAttribute("login"));
user.setFullName(oauth2User.getAttribute("name"));
user.setEmail(oauth2User.getAttribute("email"));
user.setProviderUrl(oauth2User.getAttribute("html_url"));
user.setAvatarUrl(oauth2User.getAttribute("avatar_url"));
user.setProvider(authUser.getProviderId());
user.setAuthId(authUser.getAuthId());
user.setLoginName(authUser.getLoginName());
user.setFullName(authUser.getFullName());
user.setEmail(authUser.getEmail());
user.setProviderUrl(authUser.getProviderUrl());
user.setAvatarUrl(authUser.getAvatarUrl());
entityManager.persist(user);
return user;
}

@Transactional
public UserData updateExistingUser(UserData user, OAuth2User oauth2User) {
if ("github".equals(user.getProvider())) {
public UserData updateExistingUser(UserData user, AuthUser authUser) {
if (authUser.getProviderId().equals(user.getProvider())) {
var updated = false;
String loginName = oauth2User.getAttribute("login");
String loginName = authUser.getLoginName();
if (loginName != null && !loginName.equals(user.getLoginName())) {
user.setLoginName(loginName);
updated = true;
}
String fullName = oauth2User.getAttribute("name");
String fullName = authUser.getFullName();
if (fullName != null && !fullName.equals(user.getFullName())) {
user.setFullName(fullName);
updated = true;
}
String email = oauth2User.getAttribute("email");
String email = authUser.getEmail();
if (email != null && !email.equals(user.getEmail())) {
user.setEmail(email);
updated = true;
}
String providerUrl = oauth2User.getAttribute("html_url");
String providerUrl = authUser.getProviderUrl();
if (providerUrl != null && !providerUrl.equals(user.getProviderUrl())) {
user.setProviderUrl(providerUrl);
updated = true;
}
String avatarUrl = oauth2User.getAttribute("avatar_url");
String avatarUrl = authUser.getAvatarUrl();
if (avatarUrl != null && !avatarUrl.equals(user.getAvatarUrl())) {
user.setAvatarUrl(avatarUrl);
updated = true;
Expand Down Expand Up @@ -334,4 +334,4 @@ public ResultJson deleteAccessToken(UserData user, long id) {
public boolean canLogin() {
return clientRegistrationRepository != null && clientRegistrationRepository.findByRegistrationId("github") != null;
}
}
}
48 changes: 48 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/security/AuthUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/********************************************************************************
* Copyright (c) 2023 Ericsson and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
package org.eclipse.openvsx.security;

/**
* Encapsulate information about freshly authenticated users.
*
* Different OAuth2 providers may return the same information with different
* attribute keys. This interface allows bridging arbitrary providers.
*/
public interface AuthUser {
/**
* @return Non-human readable unique identifier.
*/
String getAuthId();
/**
* @return The user's avatar URL. Some services require post-processing to get the actual value for it
* (the value returned is a template and you need to remplace variables).
*/
String getAvatarUrl();
/**
* @return The user's email.
*/
String getEmail();
/**
* @return The user's full name (first and last names).
*/
String getFullName();
/**
* @return The login name for the user. Human-readable unique name. AKA username.
*/
String getLoginName();
/**
* @return The authentication provider unique name, e.g. `github`, `eclipse`, etc.
*/
String getProviderId();
/**
* @return The authentication provider URL.
*/
String getProviderUrl();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/********************************************************************************
* Copyright (c) 2023 Ericsson and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
package org.eclipse.openvsx.security;

import org.eclipse.openvsx.OVSXConfig;
import org.eclipse.openvsx.OVSXConfig.OAuth2Config.AttributeNames;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class AuthUserFactory {

protected static final Map<String, AttributeNames> DEFAULTS = new HashMap<>();

public static class MissingProvider extends Exception {
public MissingProvider(String provider) { super("Missing configuration: ovsx.auth.attribute-names." + provider); }
}

static {
var github = new AttributeNames();
github.setAvatarUrl("avatar_url");
github.setEmail("email");
github.setFullName("name");
github.setLoginName("login");
github.setProviderUrl("html_url");
DEFAULTS.put("github", github);
}

protected final OVSXConfig config;

public AuthUserFactory(OVSXConfig config) {
this.config = config;
}

/**
* @param provider The configured OAuth2 provider from which the user object came from.
* @param user The OAuth2 user object to get attributes from.
* @return An {@link AuthUser} instance with attributes set according to the current configuration.
* @throws MissingProvider if an attribute name mapping is missing for the given provider.
*/
public AuthUser createAuthUser(String provider, OAuth2User user) throws MissingProvider {
var attr = getAttributeNames(provider);
return new DefaultAuthUser(
user.getName(),
getAttribute(user, attr.getAvatarUrl()),
getAttribute(user, attr.getEmail()),
getAttribute(user, attr.getFullName()),
getAttribute(user, attr.getLoginName()),
provider,
getAttribute(user, attr.getProviderUrl())
);
}

protected <T> T getAttribute(OAuth2User oauth2User, String attribute) {
return attribute == null ? null : oauth2User.getAttribute(attribute);
}

/**
* @param provider The provider to get the attribute mappings for.
* @return The relevant attribute mappings.
*/
protected AttributeNames getAttributeNames(String provider) throws MissingProvider {
var attributeNames = config.getOauth2().getAttributeNames().get(provider);
if (attributeNames == null) attributeNames = DEFAULTS.get(provider);
if (attributeNames == null) throw new MissingProvider(provider);
return attributeNames;
}
}
Loading

0 comments on commit 3d91eed

Please sign in to comment.