From ad76b51c7850699465467e20e75462b13a4cc02b Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Fri, 23 Oct 2026 20:16:14 +0200 Subject: [PATCH] Support nodebb-plugin-sso-oauth2-multiple for NodeBB user lookup Add NODEBB_SSO_PLUGIN (LEGACY|OAUTH2_MULTIPLE, default LEGACY) to switch the user lookup between nodebb-plugin-sso-oauth-faforever and nodebb-plugin-sso-oauth2-multiple without a redeploy of the code. Co-Authored-By: Claude Sonnet 5.5 --- configuration.md | 2 + .../api/config/FafApiProperties.java | 20 ++++++++++ .../faforever/api/forum/NodebbService.java | 40 ++++++++++++++++--- src/main/resources/config/application.yml | 2 + 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/configuration.md b/configuration.md index 10b4da809..779e79b23 100644 --- a/configuration.md +++ b/configuration.md @@ -59,6 +59,8 @@ | NODEBB_ADMIN_USERID | `1` | | | | NODEBB_BASE_URL | `false` | | | | NODEBB_MASTER_TOKEN | `false` | | | +| NODEBB_SSO_PLUGIN | `LEGACY` | `LEGACY`, `OAUTH2_MULTIPLE` | | +| NODEBB_SSO_PROVIDER_NAME | `faf-nodebb` | | | | PASSWORD_RESET_EMAIL_BODY | `Reset email body for user {0} with reset link {1}` | | `Reset email body for user {0} with reset link {1}` | | PASSWORD_RESET_EMAIL_SUBJECT | `FAF password reset` | | `FAF password reset` | | PASSWORD_RESET_URL_FORMAT | `https://www.${FAF_DOMAIN}/account/password/confirmReset?username=%s&token=%s` | `FAF_DOMAIN` | `http://localhost:8020/account/password/confirmReset?username=%s&token=%s` | diff --git a/src/main/java/com/faforever/api/config/FafApiProperties.java b/src/main/java/com/faforever/api/config/FafApiProperties.java index 22ad0ab44..8bcdfa2a8 100644 --- a/src/main/java/com/faforever/api/config/FafApiProperties.java +++ b/src/main/java/com/faforever/api/config/FafApiProperties.java @@ -278,6 +278,26 @@ public static class Nodebb { */ private int adminUserId; private String masterToken; + /** + * The name of the OAuth2 strategy configured in nodebb-plugin-sso-oauth2-multiple. It is used as provider name + * to look up forum users by their FAF user id. + */ + private String ssoProviderName = "faf-nodebb"; + /** + * Which NodeBB SSO plugin is installed. Allows switching between the plugins without redeploying the API. + */ + private SsoPlugin ssoPlugin = SsoPlugin.LEGACY; + + public enum SsoPlugin { + /** + * nodebb-plugin-sso-oauth-faforever + */ + LEGACY, + /** + * nodebb-plugin-sso-oauth2-multiple + */ + OAUTH2_MULTIPLE + } } @Data diff --git a/src/main/java/com/faforever/api/forum/NodebbService.java b/src/main/java/com/faforever/api/forum/NodebbService.java index 9b09a553a..b049bdfdb 100644 --- a/src/main/java/com/faforever/api/forum/NodebbService.java +++ b/src/main/java/com/faforever/api/forum/NodebbService.java @@ -53,16 +53,35 @@ public void userDataChanged(UserUpdatedEvent event) { } private Optional getNodebbUserId(int userId) { - URI uri = UriComponentsBuilder.fromUriString(properties.getNodebb().getBaseUrl()) - // This is not an official NodeBB api url, it's coming from our own sso plugin - .pathSegment("api", "v3", "plugins", "sso", "user", String.valueOf(userId)) + FafApiProperties.Nodebb nodebb = properties.getNodebb(); + boolean legacy = nodebb.getSsoPlugin() == FafApiProperties.Nodebb.SsoPlugin.LEGACY; + + // These are not official NodeBB api urls, they are provided by our sso plugins + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(nodebb.getBaseUrl()); + if (legacy) { + // nodebb-plugin-sso-oauth-faforever + builder.pathSegment("api", "v3", "plugins", "sso", "user", String.valueOf(userId)); + } else { + // nodebb-plugin-sso-oauth2-multiple + builder.pathSegment("api", "v3", "plugins", "oauth2-multiple", "provider", nodebb.getSsoProviderName(), + "user", String.valueOf(userId)); + } + URI uri = builder .queryParam("_uid", getAdminUserId()) .build() .toUri(); try { - ResponseEntity result = restTemplate.exchange(uri, HttpMethod.GET, buildAuthorizedRequest(null), UserResponse.class); - return Optional.ofNullable(result.getBody()).map(UserResponse::uid); + if (legacy) { + ResponseEntity result = restTemplate.exchange(uri, HttpMethod.GET, buildAuthorizedRequest(null), UserResponse.class); + return Optional.ofNullable(result.getBody()).map(UserResponse::uid); + } + + ResponseEntity result = restTemplate.exchange(uri, HttpMethod.GET, buildAuthorizedRequest(null), LookupResponse.class); + return Optional.ofNullable(result.getBody()) + .map(LookupResponse::response) + .map(LookupResponsePayload::userData) + .map(UserResponse::uid); } catch (HttpClientErrorException e) { if (e.getStatusCode() == HttpStatus.NOT_FOUND) { log.debug("User id {} not found in NodeBB. Probably the user never logged in there and has no account there.", userId); @@ -106,6 +125,17 @@ private HttpEntity buildAuthorizedRequest(T payload) { return new HttpEntity<>(payload, HttpHeaders.readOnlyHttpHeaders(headers)); } + /** + * Response of nodebb-plugin-sso-oauth2-multiple. NodeBB wraps api responses as {"status": {...}, "response": {...}} + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public record LookupResponse(LookupResponsePayload response) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record LookupResponsePayload(UserResponse userData) { + } + @JsonIgnoreProperties(ignoreUnknown = true) public record UserResponse(Integer uid, String username) { } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 1084a55af..7757aa038 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -58,6 +58,8 @@ faf-api: base-url: ${NODEBB_BASE_URL:false} admin-user-id: ${NODEBB_ADMIN_USERID:1} master-token: ${NODEBB_MASTER_TOKEN:false} + sso-plugin: ${NODEBB_SSO_PLUGIN:LEGACY} + sso-provider-name: ${NODEBB_SSO_PROVIDER_NAME:faf-nodebb} password-reset: password-reset-url-format: ${PASSWORD_RESET_URL_FORMAT:https://www.${FAF_DOMAIN}/account/password/confirmReset?username=%s&token=%s} subject: ${PASSWORD_RESET_EMAIL_SUBJECT:FAF password reset}