diff --git a/.env.example b/.env.example index ec0143878..4150f4862 100644 --- a/.env.example +++ b/.env.example @@ -73,10 +73,10 @@ RSD_ENVIRONMENT=prod # consumed by services: authentication, frontend (api/fe) # provide a list of supported OpenID auth providers # the values should be separated by semicolon (;) -# Allowed values are: SURFCONEXT, HELMHOLTZID, ORCID or LOCAL +# Allowed values are: SURFCONEXT, HELMHOLTZID, ORCID, AZURE, LINKEDIN or LOCAL # if env value is not provided default provider is set to be SURFCONEXT # if you add the value "LOCAL", then local accounts are enabled, USE THIS FOR TESTING PURPOSES ONLY -RSD_AUTH_PROVIDERS=SURFCONEXT;ORCID;AZURE;LOCAL +RSD_AUTH_PROVIDERS=SURFCONEXT;ORCID;AZURE;LINKEDIN;LOCAL # consumed by services: authentication, frontend (api/fe) # provide a list of supported OpenID auth providers for coupling with the user's RSD account @@ -91,27 +91,27 @@ RSD_AUTH_COUPLE_PROVIDERS=ORCID #RSD_AUTH_USER_MAIL_WHITELIST=user@example.com;test@example.com # SURFCONEXT - TEST ENVIRONMENT -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ SURFCONEXT_CLIENT_ID=www.research-software.nl -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ SURFCONEXT_REDIRECT=http://localhost/auth/login/surfconext -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ SURFCONEXT_WELL_KNOWN_URL=https://connect.test.surfconext.nl/.well-known/openid-configuration -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ SURFCONEXT_SCOPES=openid -# consumed by: frontend/utils/loginHelpers +# consumed by: frontend/pages/api/fe/auth/ SURFCONEXT_RESPONSE_MODE=form_post # Helmholtz ID -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ HELMHOLTZID_CLIENT_ID=rsd-dev -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ HELMHOLTZID_REDIRECT=http://localhost/auth/login/HELMHOLTZID -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ HELMHOLTZID_WELL_KNOWN_URL=https://login-dev.helmholtz.de/oauth2/.well-known/openid-configuration -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ HELMHOLTZID_SCOPES=openid+profile+email+eduperson_principal_name -# consumed by: frontend/utils/loginHelpers +# consumed by: frontend/pages/api/fe/auth/ HELMHOLTZID_RESPONSE_MODE=query # consumed by: authentication # uncomment if you want to allow users from non-Helmholtz centres or social IdPs: @@ -123,29 +123,29 @@ HELMHOLTZID_RESPONSE_MODE=query # HELMHOLTZID_ALLOW_LIST= # ORCID -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ ORCID_CLIENT_ID=APP-4D4D69ASWTYOI9QI -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ ORCID_REDIRECT=http://www.localhost/auth/login/orcid -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ ORCID_REDIRECT_COUPLE=http://www.localhost/auth/couple/orcid -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ ORCID_WELL_KNOWN_URL=https://sandbox.orcid.org/.well-known/openid-configuration -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ ORCID_SCOPES=openid -# consumed by: frontend/utils/loginHelpers +# consumed by: frontend/pages/api/fe/auth/ ORCID_RESPONSE_MODE=query # AZURE ACTIVE DIRECTORY -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ AZURE_CLIENT_ID= -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ AZURE_REDIRECT=http://localhost/auth/login/azure -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ AZURE_WELL_KNOWN_URL= -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ AZURE_SCOPES=openid+email+profile -# consumed by: authentication, frontend/utils/loginHelpers +# consumed by: authentication, frontend/pages/api/fe/auth/ AZURE_LOGIN_PROMPT=select_account # consumed by: frontend # the name displayed to users when multiple providers are configured @@ -157,6 +157,17 @@ AZURE_DESCRIPTION_HTML="Sign in with your institutional credentials" # the organisation recorded for users logged in via this provider AZURE_ORGANISATION= +# LINKEDIN +# consumed by: authentication, frontend/pages/api/fe/auth/ +LINKEDIN_CLIENT_ID= +# consumed by: authentication, frontend/pages/api/fe/auth/ +LINKEDIN_REDIRECT=http://localhost/auth/login/linkedin +# consumed by: authentication, frontend/pages/api/fe/auth/ +LINKEDIN_WELL_KNOWN_URL=https://www.linkedin.com/oauth/.well-known/openid-configuration + + +# ---- PUBLIC SCRAPER ENV VARIABLES ------------- + # max requests to the GitHub API per run, runs 10 times per hour # optional, comment out if not available, a default of 6 will be used # consumed by: scrapers @@ -211,6 +222,10 @@ AUTH_ORCID_CLIENT_SECRET= # consumed by services: authentication AUTH_AZURE_CLIENT_SECRET= +# LinkedIn +# consumed by services: authentication +AUTH_LINKEDIN_CLIENT_SECRET= + # consumed by: scrapers # optional, comment out if not available, should be of the form username:token # obtain the secret from GITHUB dashboard diff --git a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Config.java b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Config.java index c0b56b167..e54befed4 100644 --- a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Config.java +++ b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Config.java @@ -1,6 +1,6 @@ -// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 - 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) // SPDX-FileCopyrightText: 2022 dv4all @@ -26,10 +26,10 @@ private Config() { private static Collection rsdAuthCoupleProviders() { return Optional.ofNullable(System.getenv("RSD_AUTH_COUPLE_PROVIDERS")) - .map(String::toUpperCase) - .map(s -> s.split(";")) - .map(Set::of) - .orElse(Collections.emptySet()); + .map(String::toUpperCase) + .map(s -> s.split(";")) + .map(Set::of) + .orElse(Collections.emptySet()); } public static boolean isDevEnv() { @@ -42,10 +42,10 @@ public static boolean isDevEnv() { private static Collection rsdLoginProviders() { return Optional.ofNullable(System.getenv("RSD_AUTH_PROVIDERS")) - .map(String::toUpperCase) - .map(s -> s.split(";")) - .map(Set::of) - .orElse(Collections.emptySet()); + .map(String::toUpperCase) + .map(s -> s.split(";")) + .map(Set::of) + .orElse(Collections.emptySet()); } public static boolean isLocalLoginEnabled() { @@ -73,6 +73,10 @@ public static boolean isAzureLoginEnabled() { return rsdLoginProviders().contains("AZURE"); } + public static boolean isLinkedinLoginEnabled() { + return rsdLoginProviders().contains("LINKEDIN"); + } + public static String userMailWhitelist() { return System.getenv("RSD_AUTH_USER_MAIL_WHITELIST"); } @@ -99,10 +103,6 @@ public static String surfconextClientSecret() { return System.getenv("AUTH_SURFCONEXT_CLIENT_SECRET"); } - public static String surfconextScopes() { - return System.getenv("SURFCONEXT_SCOPES"); - } - // Helmholtz ID public static String helmholtzIdRedirect() { @@ -127,7 +127,7 @@ public static String helmholtzIdScopes() { public static boolean helmholtzIdAllowExternalUsers() { return Boolean.parseBoolean( - System.getenv("HELMHOLTZID_ALLOW_EXTERNAL_USERS") + System.getenv("HELMHOLTZID_ALLOW_EXTERNAL_USERS") ); } @@ -137,7 +137,7 @@ public static String helmholtzIdAllowList() { public static boolean helmholtzIdUseAllowList() { return Boolean.parseBoolean( - System.getenv("HELMHOLTZID_USE_ALLOW_LIST") + System.getenv("HELMHOLTZID_USE_ALLOW_LIST") ); } @@ -163,9 +163,6 @@ public static String orcidClientSecret() { return System.getenv("AUTH_ORCID_CLIENT_SECRET"); } - public static String orcidScopes() { - return System.getenv("ORCID_SCOPES"); - } // Azure Active Directory public static String azureRedirect() { @@ -184,11 +181,25 @@ public static String azureClientSecret() { return System.getenv("AUTH_AZURE_CLIENT_SECRET"); } - public static String azureScopes() { - return System.getenv("AZURE_SCOPES"); - } - public static String azureOrganisation() { return System.getenv("AZURE_ORGANISATION"); } + + + // LinkedIn + public static String linkedinRedirect() { + return System.getenv("LINKEDIN_REDIRECT"); + } + + public static String linkedinClientId() { + return System.getenv("LINKEDIN_CLIENT_ID"); + } + + public static String linkedinWellknown() { + return System.getenv("LINKEDIN_WELL_KNOWN_URL"); + } + + public static String linkedinClientSecret() { + return System.getenv("AUTH_LINKEDIN_CLIENT_SECRET"); + } } diff --git a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/LinkedinLogin.java b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/LinkedinLogin.java new file mode 100644 index 000000000..417cd6874 --- /dev/null +++ b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/LinkedinLogin.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +package nl.esciencecenter.rsd.authentication; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class LinkedinLogin implements Login { + + private final String code; + private final String redirectUrl; + + public LinkedinLogin(String code, String redirectUrl) { + this.code = Objects.requireNonNull(code); + this.redirectUrl = Objects.requireNonNull(redirectUrl); + } + + @Override + public OpenIdInfo openidInfo() throws IOException, InterruptedException, RsdResponseException { + Map form = createForm(); + String tokenResponse = getTokensFromLinkedin(form); + String idToken = extractIdToken(tokenResponse); + + DecodedJWT idJwt = JWT.decode(idToken); + String subject = idJwt.getSubject(); + String name = idJwt.getClaim("name").asString(); + if (name == null) { + String givenName = idJwt.getClaim("given_name").asString(); + String familyName = idJwt.getClaim("family_name").asString(); + if (givenName != null && familyName != null) name = givenName + " " + familyName; + else if (familyName != null) name = familyName; + else if (givenName != null) name = givenName; + } + String email = idJwt.getClaim("email").asString(); + Map> emptyData = Collections.emptyMap(); + return new OpenIdInfo(subject, name, email, null, emptyData); + } + + private Map createForm() { + Map form = new HashMap<>(); + form.put("code", code); + form.put("grant_type", "authorization_code"); + form.put("redirect_uri", redirectUrl); + form.put("client_id", Config.linkedinClientId()); + form.put("client_secret", Config.linkedinClientSecret()); + return form; + } + + private String getTokensFromLinkedin(Map form) throws IOException, InterruptedException, RsdResponseException { + URI tokenEndpoint = Utils.getTokenUrlFromWellKnownUrl(URI.create(Config.linkedinWellknown())); + return Utils.postForm(tokenEndpoint, form); + } + + private String extractIdToken(String response) { + return JsonParser.parseString(response).getAsJsonObject().getAsJsonPrimitive("id_token").getAsString(); + } +} diff --git a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Main.java b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Main.java index ff1ec555e..b54aed9d9 100644 --- a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Main.java +++ b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Main.java @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2021 - 2024 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2021 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2021 - 2025 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2021 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 - 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) @@ -14,6 +14,7 @@ import com.auth0.jwt.interfaces.DecodedJWT; import io.javalin.Javalin; import io.javalin.http.Context; +import io.javalin.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -185,6 +186,16 @@ public static void main(String[] args) { }); } + if (Config.isLinkedinLoginEnabled()) { + app.get("login/linkedin", ctx -> { + String code = ctx.queryParam("code"); + String redirectUrl = Config.linkedinRedirect(); + OpenIdInfo linkedinInfo = new LinkedinLogin(code, redirectUrl).openidInfo(); + AccountInfo accountInfo = new PostgrestAccount().account(linkedinInfo, OpenidProvider.linkedin); + createAndSetToken(ctx, accountInfo); + }); + } + app.get("/refresh", ctx -> { try { String tokenToVerify = ctx.cookie("rsd_token"); @@ -210,19 +221,19 @@ public static void main(String[] args) { app.exception(RsdAuthenticationException.class, (ex, ctx) -> { setLoginFailureCookie(ctx, ex.getMessage()); - ctx.redirect(LOGIN_FAILED_PATH); + ctx.redirect(LOGIN_FAILED_PATH, HttpStatus.SEE_OTHER); }); app.exception(RuntimeException.class, (ex, ctx) -> { LOGGER.error("RuntimeException", ex); setLoginFailureCookie(ctx, "Something unexpected went wrong, please try again or contact us."); - ctx.redirect(LOGIN_FAILED_PATH); + ctx.redirect(LOGIN_FAILED_PATH, HttpStatus.SEE_OTHER); }); app.exception(Exception.class, (ex, ctx) -> { LOGGER.error("Exception", ex); setLoginFailureCookie(ctx, "Something unexpected went wrong, please try again or contact us."); - ctx.redirect(LOGIN_FAILED_PATH); + ctx.redirect(LOGIN_FAILED_PATH, HttpStatus.SEE_OTHER); }); } @@ -245,9 +256,9 @@ static void setRedirectFromCookie(Context ctx) { String returnPath = ctx.cookie("rsd_pathname"); if (returnPath != null && !returnPath.isBlank()) { returnPath = returnPath.trim(); - ctx.redirect(returnPath); + ctx.redirect(returnPath, HttpStatus.SEE_OTHER); } else { - ctx.redirect("/"); + ctx.redirect("/", HttpStatus.SEE_OTHER); } } diff --git a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/OpenidProvider.java b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/OpenidProvider.java index f1ccaf125..39216644e 100644 --- a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/OpenidProvider.java +++ b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/OpenidProvider.java @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 Netherlands eScience Center +// SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -10,5 +10,6 @@ public enum OpenidProvider { surfconext, helmholtz, orcid, - azure + azure, + linkedin } diff --git a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Utils.java b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Utils.java index efe8e9811..7098e325c 100644 --- a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Utils.java +++ b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/Utils.java @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -7,9 +7,12 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; +import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -19,6 +22,8 @@ public class Utils { + private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); + private Utils() { } @@ -37,9 +42,13 @@ public static URI extractTokenUrlFromWellKnownData(String jsonData) { return URI.create(tokenUrl); } + public static String urlEncode(String s) { + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } + public static String formMapToxWwwFormUrlencoded(Map form) { StringJoiner x_www_form_urlencoded = new StringJoiner("&"); - form.forEach((key, value) -> x_www_form_urlencoded.add(key + "=" + value)); + form.forEach((key, value) -> x_www_form_urlencoded.add(urlEncode(key) + "=" + urlEncode(value))); return x_www_form_urlencoded.toString(); } @@ -58,6 +67,7 @@ public static String postForm(URI uri, Map form) throws IOExcept try (HttpClient client = HttpClient.newHttpClient()) { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 300) { + LOGGER.error("Error posting form: {}, {}", response.statusCode(), response.body()); throw new RsdResponseException(response.statusCode(), response.uri(), response.body(), "Error posting form"); } return response.body(); diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 408479685..5a82d7e26 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022 - 2024 Christian Meeßen (GFZ) -# SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) # SPDX-FileCopyrightText: 2022 - 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -# SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center +# SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) +# SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center # SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) # SPDX-FileCopyrightText: 2022 Helmholtz Centre for Environmental Research (UFZ) # SPDX-FileCopyrightText: 2022 dv4all @@ -66,7 +66,6 @@ services: - SURFCONEXT_CLIENT_ID - SURFCONEXT_REDIRECT - SURFCONEXT_WELL_KNOWN_URL - - SURFCONEXT_SCOPES - HELMHOLTZID_CLIENT_ID - HELMHOLTZID_REDIRECT - HELMHOLTZID_WELL_KNOWN_URL @@ -77,17 +76,19 @@ services: - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE - ORCID_WELL_KNOWN_URL - - ORCID_SCOPES - AZURE_CLIENT_ID - AZURE_REDIRECT - AZURE_WELL_KNOWN_URL - - AZURE_SCOPES - AZURE_ORGANISATION + - LINKEDIN_CLIENT_ID + - LINKEDIN_REDIRECT + - LINKEDIN_WELL_KNOWN_URL - HELMHOLTZID_ALLOW_EXTERNAL_USERS - AUTH_SURFCONEXT_CLIENT_SECRET - AUTH_HELMHOLTZID_CLIENT_SECRET - AUTH_ORCID_CLIENT_SECRET - AUTH_AZURE_CLIENT_SECRET + - AUTH_LINKEDIN_CLIENT_SECRET - PGRST_JWT_SECRET depends_on: - database @@ -130,6 +131,9 @@ services: - AZURE_LOGIN_PROMPT - AZURE_DISPLAY_NAME - AZURE_DESCRIPTION_HTML + - LINKEDIN_CLIENT_ID + - LINKEDIN_REDIRECT + - LINKEDIN_WELL_KNOWN_URL - CROSSREF_CONTACT_EMAIL expose: - 3000 diff --git a/docker-compose.yml b/docker-compose.yml index bd66a0a04..613f1bc62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2021 - 2023 Dusan Mijatovic (dv4all) # SPDX-FileCopyrightText: 2021 - 2023 dv4all -# SPDX-FileCopyrightText: 2021 - 2024 Ewan Cahen (Netherlands eScience Center) -# SPDX-FileCopyrightText: 2021 - 2024 Netherlands eScience Center +# SPDX-FileCopyrightText: 2021 - 2025 Ewan Cahen (Netherlands eScience Center) +# SPDX-FileCopyrightText: 2021 - 2025 Netherlands eScience Center # SPDX-FileCopyrightText: 2022 - 2024 Christian Meeßen (GFZ) # SPDX-FileCopyrightText: 2022 - 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences # SPDX-FileCopyrightText: 2022 Helmholtz Centre for Environmental Research (UFZ) @@ -68,7 +68,6 @@ services: - SURFCONEXT_CLIENT_ID - SURFCONEXT_REDIRECT - SURFCONEXT_WELL_KNOWN_URL - - SURFCONEXT_SCOPES - HELMHOLTZID_CLIENT_ID - HELMHOLTZID_REDIRECT - HELMHOLTZID_WELL_KNOWN_URL @@ -79,17 +78,19 @@ services: - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE - ORCID_WELL_KNOWN_URL - - ORCID_SCOPES - AZURE_CLIENT_ID - AZURE_REDIRECT - AZURE_WELL_KNOWN_URL - - AZURE_SCOPES - AZURE_ORGANISATION + - LINKEDIN_CLIENT_ID + - LINKEDIN_REDIRECT + - LINKEDIN_WELL_KNOWN_URL - HELMHOLTZID_ALLOW_EXTERNAL_USERS - AUTH_SURFCONEXT_CLIENT_SECRET - AUTH_HELMHOLTZID_CLIENT_SECRET - AUTH_ORCID_CLIENT_SECRET - AUTH_AZURE_CLIENT_SECRET + - AUTH_LINKEDIN_CLIENT_SECRET - PGRST_JWT_SECRET depends_on: - database @@ -143,6 +144,9 @@ services: - AZURE_LOGIN_PROMPT - AZURE_DISPLAY_NAME - AZURE_DESCRIPTION_HTML + - LINKEDIN_CLIENT_ID + - LINKEDIN_REDIRECT + - LINKEDIN_WELL_KNOWN_URL - CROSSREF_CONTACT_EMAIL expose: - 3000 diff --git a/documentation/docs/03-rsd-instance/02-configurations.md b/documentation/docs/03-rsd-instance/02-configurations.md index 6ecc91ee9..b4229ba8d 100644 --- a/documentation/docs/03-rsd-instance/02-configurations.md +++ b/documentation/docs/03-rsd-instance/02-configurations.md @@ -25,19 +25,20 @@ The RSD supports the following third party OpenID Connect authentication service - [ORCID](#enable-orcid-authentication-and-coupling) - [SURFconext](#enable-surfconext-authentication) - [Helmholtz AI](#enable-helmholtz-ai-authentication) +- [LinkedIn](#enable-linkedin-authentication) :::warning RSD requires one of mentioned authentication providers to be used in the production. Please obtain the required information for setting up the authentication service directly from the provider. The required information about the authentication provider is stored in `.env` file (environment variables). After changing any value in the .env file you should restart the RSD instance. ::: :::tip -You can define multiple providers for authentication in the environment variable by providing a semicolon seprated keys. +You can define multiple providers for authentication in the environment variable by providing a semicolon separated keys. RSD_AUTH_PROVIDERS=AZURE;ORCID;HELMHOLTZID;SURFCONEXT;LOCAL ::: ### Enable Microsoft Entra ID (Azure AD) authentication -Please refer to [Microsoft Entra ID documention](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal-setup-oidc-sso) about setting up the application access. In the `.env` file you need to provide following information to enable the authentication service. +Please refer to [Microsoft Entra ID documentation](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal-setup-oidc-sso) about setting up the application access. In the `.env` file you need to provide following information to enable the authentication service. ```bash # Ensure AZURE key is included in the list @@ -67,7 +68,7 @@ AZURE_ORGANISATION= ### Enable ORCID authentication and coupling -The RSD offers an integration with ORCID which can be used for login and coupling the user's RSD account with their ORCID. Both integrations can be used independently. +The RSD offers an integration with ORCID which can be used for signing in and coupling the user's RSD account with their ORCID. Both integrations can be used independently. Please refer to the [ORCID OAuth documentation](https://info.orcid.org/documentation/integration-guide/getting-started-with-your-orcid-integration/) in order to set up the ORCID authentication service for the RSD. Specifically, look [here](https://info.orcid.org/documentation/integration-guide/registering-a-public-api-client/) on how to register redirect URLs. @@ -122,11 +123,11 @@ If ORCID login is disabled and ORCID coupling is enabled, users are added to the ::: - For more [info about public profile page see documentation](/users/user-settings/#public-profile). -- If ORCID login is enabled: after a user links an ORCID to their RSD account they will be able to login using ORCID credentials too. +- If ORCID login is enabled: after a user links an ORCID to their RSD account they will be able to log in using ORCID credentials too. ### Enable SURFconext authentication -Please refer to [SURFconext documention](https://www.surf.nl/en/surfconext-global-access-with-1-set-of-credentials). +Please refer to [SURFconext documentation](https://www.surf.nl/en/surfconext-global-access-with-1-set-of-credentials). :::danger The main RSD instance is already registered with the SURFconext authentication service. We advise you to use our main RSD instance and enable it for your organisation. For more information see [register you organisation](/users/register-organisation/). @@ -136,16 +137,20 @@ The main RSD instance is already registered with the SURFconext authentication s Helmholtz already runs an RSD instance at [https://helmholtz.software/](https://helmholtz.software/) +### Enable LinkedIn authentication + +First, create an app on [https://developer.linkedin.com/](https://developer.linkedin.com/). Follow the steps [here](https://www.linkedin.com/help/linkedin/answer/a1665329) to get your app approved by the company you linked it to. Copy the related environment variables from `.env.example` to your `.env` and fill in the missing values (don't forget to set your custom domain for `LINKEDIN_REDIRECT`). Finally, add `LINKEDIN` to the values in the environment variable `RSD_AUTH_PROVIDERS`. + ## Host definitions The `host` section of settings.json defines following settings. **Most of them should be customised**. -- `name`: default value is `rsd`. It is used to load default RSD homepage layout. The other two options are `helmholtz` and `imperial` which will load the homepage layout of these insitutions. -- `email`: the email shown in the footer of the RSD pages. It shoud be used as public contact email. **Change this value to reflect your contact email**. +- `name`: default value is `rsd`. It is used to load default RSD homepage layout. The other two options are `helmholtz` and `imperial` which will load the homepage layout of these institutions. +- `email`: the email shown in the footer of the RSD pages. It should be used as public contact email. **Change this value to reflect your contact email**. - `emailHeaders`: append custom email headers when user tries to contact you. - `logo_url`: the url to footer logo. You can change this value provided you also mount the image - `website`: the url that will be attached to footer logo -- `feedback`: in the header of the RSD there is a feedback button. The feedback button can be enabled. In addition the `url` value is actually the email. In the form there is also link to RSD issues page if the user wants to create an Github issue concerning his/her feedback. +- `feedback`: in the header of the RSD there is a feedback button. The feedback button can be enabled. In addition the `url` value is actually the email. In the form there is also link to RSD issues page if the user wants to create an GitHub issue concerning his/her feedback. - `login_info_url`: the link to getting access documentation shown in the "Sign in with" modal. It is relevant only if you use more than one authentication provider. If you use only one authentication provider "Sign in with" modal is not used, instead the user is directly redirected to authentication page. - `terms_of_service_url`: the link to your Terms of Service page. Used on the user profile page to let user accept the terms of service - `privacy_statement_url`: the link to your privacy statement page. Used on the user profile page to let user accept the privacy statement. @@ -193,7 +198,7 @@ The look and feel of RSD can be customised with desired colors and fonts. In the - The `settings.json` should be mounted into `/app/public/data` folder of frontend service - The `index.css` should be mounted into `/app/public/styles` folder of frontend service -- When customizing RSD styles we advice to mount custom fonts in the styles folder close to index.css +- When customizing RSD styles we advise to mount custom fonts in the styles folder close to index.css - The footer logo should be mounted into `/app/public/images`. Then you can use relative image path `/images/your-logo.svg` in the settings.json - Your [starting point should be our default files](https://github.com/research-software-directory/RSD-as-a-service/tree/main/frontend/public) where you adjust the values you want to be different. diff --git a/documentation/docs/03-rsd-instance/02-configurations.md.license b/documentation/docs/03-rsd-instance/02-configurations.md.license index b4a2c9483..c790576d8 100644 --- a/documentation/docs/03-rsd-instance/02-configurations.md.license +++ b/documentation/docs/03-rsd-instance/02-configurations.md.license @@ -1,6 +1,6 @@ SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) -SPDX-FileCopyrightText: 2023 - 2024 Ewan Cahen (Netherlands eScience Center) -SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +SPDX-FileCopyrightText: 2023 - 2025 Ewan Cahen (Netherlands eScience Center) +SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) SPDX-FileCopyrightText: 2024 Dusan Mijatovic (dv4all) (dv4all) SPDX-FileCopyrightText: 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences diff --git a/frontend/auth/api/authEndpoint.ts b/frontend/auth/api/authEndpoint.ts index f9c5328f6..29b6539bc 100644 --- a/frontend/auth/api/authEndpoint.ts +++ b/frontend/auth/api/authEndpoint.ts @@ -1,12 +1,13 @@ +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 import logger from '~/utils/logger' import {getAuthorisationEndpoint} from './authHelpers' -type providers = 'surfconext'|'helmholtzid'|'orcid'|'azure' +type providers = 'surfconext'|'helmholtzid'|'orcid'|'azure'|'linkedin' // how often we refresh auth endpoint const refreshInterval = 60*60*1000 // save timer as public variable diff --git a/frontend/pages/api/fe/auth/index.ts b/frontend/pages/api/fe/auth/index.ts index 926e402d6..c2cba9f52 100644 --- a/frontend/pages/api/fe/auth/index.ts +++ b/frontend/pages/api/fe/auth/index.ts @@ -1,6 +1,6 @@ -// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 dv4all // SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) @@ -23,6 +23,7 @@ import {helmholtzInfo} from './helmholtzid' import {localInfo} from './local' import {orcidInfo} from './orcid' import {azureInfo} from './azure' +import {linkedinInfo} from './linkedin' import logger from '~/utils/logger' export type ApiError = { @@ -52,6 +53,8 @@ async function getRedirectInfo(provider: string) { return orcidInfo() case 'azure': return azureInfo() + case 'linkedin': + return linkedinInfo() default: const message = `${provider} NOT SUPPORTED, check your spelling` logger(`api/fe/auth/providers: ${message}`, 'error') diff --git a/frontend/pages/api/fe/auth/linkedin.ts b/frontend/pages/api/fe/auth/linkedin.ts new file mode 100644 index 000000000..22ee5ff84 --- /dev/null +++ b/frontend/pages/api/fe/auth/linkedin.ts @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {getAuthEndpoint} from '~/auth/api/authEndpoint' +import {getRedirectUrl, RedirectToProps} from '~/auth/api/authHelpers' +import logger from '~/utils/logger' + +export async function linkedinInfo() { + const redirectProps = await linkedinRedirectProps() + if (!redirectProps) { + return null + } + + const redirectUrl = getRedirectUrl(redirectProps) + + return { + name: 'LinkedIn', + redirectUrl, + html: 'Sign in with your LinkedIn account' + } +} + +async function linkedinRedirectProps() { + try { + // extract well known url from env + const wellknownUrl = process.env.LINKEDIN_WELL_KNOWN_URL + if (!wellknownUrl) { + const message = 'LINKEDIN_WELL_KNOWN_URL is missing' + logger(`linkedinRedirectProps: ${message}`, 'error') + return null + } + + const authorization_endpoint = await getAuthEndpoint(wellknownUrl, 'linkedin') + if (!authorization_endpoint) { + const message = 'authorization_endpoint is missing' + logger(`linkedinRedirectProps: ${message}`, 'error') + return null + } + + const redirect_uri = process.env.LINKEDIN_REDIRECT + if (!redirect_uri) { + const message = 'LINKEDIN_REDIRECT is missing' + logger(`linkedinRedirectProps: ${message}`, 'error') + return null + } + + const client_id = process.env.LINKEDIN_CLIENT_ID + if (!client_id) { + const message = 'LINKEDIN_CLIENT_ID is missing' + logger(`linkedinRedirectProps: ${message}`, 'error') + return null + } + + const props: RedirectToProps = { + authorization_endpoint, + redirect_uri, + client_id, + scope: 'openid%20profile%20email', + response_mode: 'code' + } + return props + } catch (e: any) { + logger(`orcidRedirectProps: ${e.message}`, 'error') + return null + } +}