Skip to content

Commit

Permalink
Merge pull request #1057 from research-software-directory/879-orcid-p…
Browse files Browse the repository at this point in the history
…age-opt-in

879 - public profile page
  • Loading branch information
dmijatovic authored Dec 1, 2023
2 parents a3bec85 + 3780ff7 commit a0a700c
Show file tree
Hide file tree
Showing 128 changed files with 2,705 additions and 748 deletions.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ HELMHOLTZAAI_RESPONSE_MODE=query
# consumed by: authentication, frontend/utils/loginHelpers
ORCID_CLIENT_ID=APP-4D4D69ASWTYOI9QI
# consumed by: authentication, frontend/utils/loginHelpers
ORCID_REDIRECT=http://localhost/auth/login/orcid
ORCID_REDIRECT=http://www.localhost/auth/login/orcid
# consumed by: authentication, frontend/utils/loginHelpers
ORCID_REDIRECT_COUPLE=http://www.localhost/auth/couple/orcid
# consumed by: authentication, frontend/utils/loginHelpers
ORCID_WELL_KNOWN_URL=https://sandbox.orcid.org/.well-known/openid-configuration
# consumed by: authentication, frontend/utils/loginHelpers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ public static String orcidRedirect() {
return System.getenv("ORCID_REDIRECT");
}

public static String orcidRedirectCouple() {
return System.getenv("ORCID_REDIRECT_COUPLE");
}

public static String orcidClientId() {
return System.getenv("ORCID_CLIENT_ID");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2022 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

Expand All @@ -9,6 +9,7 @@
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Objects;

Expand All @@ -20,10 +21,12 @@ public JwtVerifier(String signingSecret) {
this.signingSecret = Objects.requireNonNull(signingSecret);
}

void verify(String token) {
if (token == null) throw new JWTVerificationException("Token was null");
DecodedJWT verify(String token) {
if (token == null) {
throw new JWTVerificationException("Token was null");
}
Algorithm signingAlgorithm = Algorithm.HMAC256(signingSecret);
JWTVerifier verifier = JWT.require(signingAlgorithm).build();
verifier.verify(token);
return verifier.verify(token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
package nl.esciencecenter.rsd.authentication;

import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import io.javalin.Javalin;
import io.javalin.http.Context;

import java.util.Base64;
import java.util.UUID;

public class Main {
static final long ONE_HOUR_IN_SECONDS = 3600; // 60 * 60
Expand Down Expand Up @@ -142,6 +144,24 @@ public static void main(String[] args) {
AccountInfo accountInfo = new PostgrestCheckOrcidWhitelistedAccount(new PostgrestAccount()).account(orcidInfo, OpenidProvider.orcid);
createAndSetToken(ctx, accountInfo);
});

app.get("/couple/orcid", ctx -> {
String code = ctx.queryParam("code");
String redirectUrl = Config.orcidRedirectCouple();
OpenIdInfo orcidInfo = new OrcidLogin(code, redirectUrl).openidInfo();

String tokenToVerify = ctx.cookie("rsd_token");
String signingSecret = Config.jwtSigningSecret();
JwtVerifier verifier = new JwtVerifier(signingSecret);
DecodedJWT decodedJWT = verifier.verify(tokenToVerify);
UUID accountId = UUID.fromString(decodedJWT.getClaim("account").asString());

new PostgrestAccount().coupleLogin(accountId, orcidInfo, OpenidProvider.orcid);

PostgrestConnector.addOrcidToAllowList(orcidInfo.sub());

setRedirectFromCookie(ctx);
});
}

if (Config.isAzureEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,37 @@ else if (accountsWithSub.size() == 1) {
String newAccountJson = postJsonAsAdmin(createAccountEndpoint, "{}", token);
String newAccountId = JsonParser.parseString(newAccountJson).getAsJsonArray().get(0).getAsJsonObject().getAsJsonPrimitive("id").getAsString();

// Create login for account, i.e., tie the login credentials to the newly created account.
JsonObject loginForAccountData = new JsonObject();
loginForAccountData.addProperty("account", newAccountId);
loginForAccountData.addProperty("sub", subject);
loginForAccountData.addProperty("name", openIdInfo.name());
loginForAccountData.addProperty("email", openIdInfo.email());
loginForAccountData.addProperty("home_organisation", openIdInfo.organisation());
loginForAccountData.addProperty("provider", provider.toString());
URI createLoginUri = URI.create(backendUri + "/login_for_account");
postJsonAsAdmin(createLoginUri, loginForAccountData.toString(), token);
createLoginForAccount(UUID.fromString(newAccountId), openIdInfo, provider, backendUri, token);

return new AccountInfo(UUID.fromString(newAccountId), openIdInfo.name(), false);
}
}

public void coupleLogin(UUID accountId, OpenIdInfo openIdInfo, OpenidProvider provider) throws IOException, InterruptedException {
String backendUri = Config.backendBaseUrl();
JwtCreator jwtCreator = new JwtCreator(Config.jwtSigningSecret());
String adminJwt = jwtCreator.createAdminJwt();
createLoginForAccount(accountId, openIdInfo, provider, backendUri, adminJwt);
}

private void createLoginForAccount(UUID accountId, OpenIdInfo openIdInfo, OpenidProvider provider, String backendUri, String adminJwt) throws IOException, InterruptedException {
JsonObject loginForAccountData = new JsonObject();
loginForAccountData.addProperty("account", accountId.toString());
loginForAccountData.addProperty("sub", openIdInfo.sub());
loginForAccountData.addProperty("name", openIdInfo.name());
loginForAccountData.addProperty("email", openIdInfo.email());
loginForAccountData.addProperty("home_organisation", openIdInfo.organisation());
loginForAccountData.addProperty("provider", provider.toString());
URI createLoginUri = URI.create(backendUri + "/login_for_account");

HttpResponse<String> response = postJsonAsAdminWithResponse(createLoginUri, loginForAccountData.toString(), adminJwt);
if (response.statusCode() == 409) {
throw new RsdAuthenticationException("This login is already coupled to an account.");
} else if (response.statusCode() >= 300) {
throw new RuntimeException("Error fetching data from the endpoint: %s with status code %d and response: %s".formatted(createLoginUri.toString(), response.statusCode(), response.body()));
}
}

private void updateLoginForAccount(UUID id, OpenIdInfo openIdInfo, String token) throws IOException, InterruptedException {
JsonObject loginForAccountData = new JsonObject();
loginForAccountData.addProperty("name", openIdInfo.name());
Expand Down Expand Up @@ -110,20 +126,27 @@ static String getAsAdmin(URI uri, String token) throws IOException, InterruptedE
}
}

private String postJsonAsAdmin(URI uri, String json, String token) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
public static String postJsonAsAdmin(URI uri, String json, String token, String... headers) throws IOException, InterruptedException {
HttpResponse<String> response = postJsonAsAdminWithResponse(uri, json, token, headers);
if (response.statusCode() >= 300) {
throw new RuntimeException("Error fetching data from the endpoint: %s with status code %d and response: %s".formatted(uri.toString(), response.statusCode(), response.body()));
}
return response.body();
}

public static HttpResponse<String> postJsonAsAdminWithResponse(URI uri, String json, String token, String... headers) throws IOException, InterruptedException {
HttpRequest.Builder httpRequestBuilder = HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofString(json))
.uri(uri)
.header("Content-Type", "application/json")
.header("Prefer", "return=representation")
.header("Authorization", "bearer " + token)
.build();
.header("Authorization", "bearer " + token);
if (headers != null && headers.length > 0 && headers.length % 2 == 0) {
httpRequestBuilder.headers(headers);
}
HttpRequest request = httpRequestBuilder.build();
try (HttpClient client = HttpClient.newHttpClient()) {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 300) {
throw new RuntimeException("Error fetching data from the endpoint: " + uri.toString() + " with response: " + response.body());
}
return response.body();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2023 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

package nl.esciencecenter.rsd.authentication;

import com.google.gson.JsonObject;

import java.io.IOException;
import java.net.URI;

public class PostgrestConnector {

public static void addOrcidToAllowList(String orcid) throws IOException, InterruptedException {
String backendUri = Config.backendBaseUrl();
URI allowListUri = URI.create(backendUri + "/orcid_whitelist");
JwtCreator jwtCreator = new JwtCreator(Config.jwtSigningSecret());
String adminJwt = jwtCreator.createAdminJwt();

JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("orcid", orcid);

PostgrestAccount.postJsonAsAdmin(allowListUri, jsonObject.toString(), adminJwt, "Prefer", "resolution=ignore-duplicates");
}
}
97 changes: 68 additions & 29 deletions data-generation/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,28 +359,42 @@ async function generateProjects(amount=500) {
return result;
}

async function generateContributors(ids, amount=1000) {
function generateOrcids(amount=50) {
const orcids = new Set();

while (orcids.size < amount) {
orcids.add(faker.helpers.replaceSymbolWithNumber('0000-000#-####-####'));
}

return [...orcids];
}

async function generateContributors(softwareIds, orcids, minPerSoftware=0, maxPerSoftware=15) {
const result = [];

for (let index = 0; index < amount; index++) {
result.push({
software: faker.helpers.arrayElement(ids),
is_contact_person: !!faker.helpers.maybe(() => true, {probability: 0.2}),
email_address: faker.internet.email(),
family_names: faker.name.lastName(),
given_names: faker.name.firstName(),
affiliation: faker.company.name(),
role: faker.name.jobTitle(),
orcid: faker.helpers.replaceSymbolWithNumber('####-####-####-####'),
avatar_id: localImageIds[index % localImageIds.length],
});
for (const softwareId of softwareIds) {
const amount = faker.mersenne.rand(maxPerSoftware, minPerSoftware);

for (let i = 0; i < amount; i++) {
result.push({
software: softwareId,
is_contact_person: !!faker.helpers.maybe(() => true, {probability: 0.2}),
email_address: faker.internet.email(),
family_names: faker.name.lastName(),
given_names: faker.name.firstName(),
affiliation: faker.company.name(),
role: faker.name.jobTitle(),
orcid: faker.helpers.maybe(() => faker.helpers.arrayElement(orcids), {probability: 0.8}) ?? null,
avatar_id: localImageIds[i % localImageIds.length],
});
}
}

return result;
}

async function generateTeamMembers(ids, amount=1000) {
const result = await generateContributors(ids, amount);
async function generateTeamMembers(projectIds, orcids, minPerProject=0, maxPerProject=15) {
const result = await generateContributors(projectIds, orcids, minPerProject, maxPerProject);
result.forEach(contributor => {
contributor['project'] = contributor['software'];
delete contributor['software'];
Expand Down Expand Up @@ -607,12 +621,21 @@ async function downloadAndGetImages(urlGenerator, amount) {
return ids
}

async function postAccountsToBackend(amount=50) {
return postToBackend('/account', new Array(amount).fill({}));
async function postAccountsToBackend(amount=100) {
const accounts = [];
for (let i = 0; i < amount; i++) {
accounts.push({
public_orcid_profile: !!faker.helpers.maybe(() => true, {probability: 0.8}),
agree_terms: !!faker.helpers.maybe(() => true, {probability: 0.8}),
notice_privacy_statement: !!faker.helpers.maybe(() => true, {probability: 0.8}),
});
}

return postToBackend('/account', accounts);
}

// Generate one login_for_account per given account
function generateLoginForAccount(accountIds) {
function generateLoginForAccount(accountIds, orcids) {
const homeOrganisations = [null];
for (let i=0; i < 10; i++) {
homeOrganisations.push("Organisation for " + faker.word.noun());
Expand All @@ -624,27 +647,43 @@ function generateLoginForAccount(accountIds) {
"ip4"
];

let orcidsAdded = 0;
const login_for_accounts = [];
accountIds.forEach(accountId => {
let firstName = faker.name.firstName();
let givenName = faker.name.lastName();
login_for_accounts.push({
account: accountId,
name: firstName + ' ' + givenName,
email: faker.internet.email(firstName, givenName),
sub: faker.random.alphaNumeric(30),
provider: faker.helpers.arrayElement(providers),
home_organisation: faker.helpers.arrayElement(homeOrganisations)
});

if (orcidsAdded < orcids.length) {
const orcid = orcids[orcidsAdded];
orcidsAdded += 1;
login_for_accounts.push({
account: accountId,
name: firstName + ' ' + givenName,
email: faker.internet.email(firstName, givenName),
sub: orcid,
provider: 'orcid',
home_organisation: faker.helpers.arrayElement(homeOrganisations)
});
} else {
login_for_accounts.push({
account: accountId,
name: firstName + ' ' + givenName,
email: faker.internet.email(firstName, givenName),
sub: faker.random.alphaNumeric(30),
provider: faker.helpers.arrayElement(providers),
home_organisation: faker.helpers.arrayElement(homeOrganisations)
});
}
})
return login_for_accounts;
}

const orcids = generateOrcids();
await postAccountsToBackend(100)
.then(() => getFromBackend('/account'))
.then(res => res.json())
.then(jsonAccounts => jsonAccounts.map(a => a.id))
.then(async accountIds => postToBackend('/login_for_account', generateLoginForAccount(accountIds)))
.then(async accountIds => postToBackend('/login_for_account', generateLoginForAccount(accountIds, orcids)))
.then(() => console.log('accounts, login_for_accounts done'));

const localImageIds = await getLocalImageIds(images);
Expand Down Expand Up @@ -673,7 +712,7 @@ const softwarePromise = postToBackend('/software', await generateSofware())
idsSoftware = swArray.map(sw => sw['id']);
idsFakeSoftware = swArray.filter(sw => sw['brand_name'].startsWith('Software')).map(sw => sw['id']);
idsRealSoftware = swArray.filter(sw => sw['brand_name'].startsWith('Real software')).map(sw => sw['id']);
postToBackend('/contributor', await generateContributors(idsSoftware));
postToBackend('/contributor', await generateContributors(idsSoftware, orcids));
postToBackend('/testimonial', generateTestimonials(idsSoftware));
postToBackend('/repository_url', generateRepositoryUrls(idsSoftware));
postToBackend('/package_manager', generatePackageManagers(idsRealSoftware));
Expand All @@ -687,7 +726,7 @@ const projectPromise = postToBackend('/project', await generateProjects())
.then(resp => resp.json())
.then(async pjArray => {
idsProjects = pjArray.map(sw => sw['id']);
postToBackend('/team_member', await generateTeamMembers(idsProjects));
postToBackend('/team_member', await generateTeamMembers(idsProjects, orcids));
postToBackend('/url_for_project', generateUrlsForProjects(idsProjects));
postToBackend('/keyword_for_project', generateKeywordsForEntity(idsProjects, idsKeywords, 'project'));
postToBackend('/output_for_project', generateMentionsForEntity(idsProjects, idsMentions, 'project'));
Expand Down
Loading

0 comments on commit a0a700c

Please sign in to comment.