Skip to content
This repository was archived by the owner on Aug 6, 2024. It is now read-only.

Commit 34b9954

Browse files
committed
Merge tag 'v1.10.0' into 79-update_1.10.0
2 parents 954e348 + dbfbe7e commit 34b9954

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+577
-391
lines changed

CITATION.cff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,5 @@ keywords:
7575
- Software Impact
7676
- Software Reuse
7777
license: Apache-2.0
78-
version: v1.9.0
79-
date-released: '2022-10-28'
78+
version: v1.10.0
79+
date-released: '2022-11-04'

authentication/README.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
11
<!--
22
SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
3+
SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) <[email protected]>
4+
SPDX-FileCopyrightText: 2022 Netherlands eScience Center
35
SPDX-FileCopyrightText: 2022 dv4all
46
57
SPDX-License-Identifier: CC-BY-4.0
68
-->
79

810
# Authentication module
911

10-
This modules handles authentication from third parties using oAuth2 and OpenID.
12+
This module handles authentication from third parties using oAuth2 and OpenID.
1113

1214
## Environment variables
15+
Check `.env.example` to see which environment variables are needed.
16+
17+
## Developing locally
18+
If you want to develop and run the auth module locally, i.e. outside of Docker, you have to make two changes to files tracked by Git.
19+
1. In `docker-compose.yml`, add the following lines to the `nginx` service:
20+
```yml
21+
extra_hosts:
22+
- "host.docker.internal:host-gateway"
23+
```
24+
2. In `nginx.conf`, replace `server auth:7000;` with `server host.docker.internal:7000;`
1325

14-
It requires the following variables at run time.
15-
16-
```env
17-
# connection to backend
18-
POSTGREST_URL=
19-
20-
# SURFconext
21-
NEXT_PUBLIC_SURFCONEXT_CLIENT_ID=
22-
NEXT_PUBLIC_SURFCONEXT_REDIRECT=
23-
AUTH_SURFCONEXT_CLIENT_SECRET=
26+
Remember to undo these changes before committing!
2427

25-
# JWT secret for postgREST
26-
PGRST_JWT_SECRET=
27-
```
28+
It is recommended to use the [envFile plugin](https://plugins.jetbrains.com/plugin/7861-envfile) of IntelliJ IDEA to load your `.env` file.
29+
Furthermore, set the value of `POSTGREST_URL` to `http://localhost/api/v1`.
2830

2931
## Running the tests
3032

authentication/src/main/java/nl/esciencecenter/rsd/authentication/Main.java

Lines changed: 59 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,28 @@
1212
import com.auth0.jwt.exceptions.JWTVerificationException;
1313
import io.javalin.Javalin;
1414
import io.javalin.http.Context;
15-
import io.javalin.http.ForbiddenResponse;
16-
import io.javalin.http.RedirectResponse;
1715

1816
import java.util.Base64;
1917

2018
public class Main {
2119
static final long ONE_HOUR_IN_SECONDS = 3600; // 60 * 60
20+
static final long ONE_MINUTE_IN_SECONDS = 60;
2221

23-
public static boolean userIsAllowed (OpenIdInfo info) {
22+
public static boolean userIsAllowed(OpenIdInfo info) {
2423
String whitelist = Config.userMailWhitelist();
2524

2625
if (whitelist == null || whitelist.length() == 0) {
2726
// allow any user
2827
return true;
2928
}
3029

31-
if (
32-
info == null || info.email() == null || info.email().length() == 0
33-
) {
30+
if (info == null || info.email() == null || info.email().length() == 0) {
3431
throw new Error("Unexpected parameters for 'userIsAllowed'");
3532
}
3633

3734
String[] allowed = whitelist.split(";");
3835

39-
for (String s: allowed) {
36+
for (String s : allowed) {
4037
if (s.equalsIgnoreCase(info.email())) {
4138
return true;
4239
}
@@ -54,83 +51,63 @@ public static void main(String[] args) {
5451
System.out.println("Warning: local accounts are enabled, this is not safe for production!");
5552
System.out.println("********************");
5653
app.post("/login/local", ctx -> {
57-
try {
58-
String sub = ctx.formParam("sub");
59-
if (sub == null || sub.isBlank()) throw new RuntimeException("Please provide a username");
60-
String name = sub;
61-
String email = sub + "@example.com";
62-
String organisation = "Example organisation";
63-
OpenIdInfo localInfo = new OpenIdInfo(sub, name, email, organisation);
64-
65-
AccountInfo accountInfo = new PostgrestAccount().account(localInfo, OpenidProvider.local);
66-
boolean isAdmin = isAdmin(email);
67-
createAndSetToken(ctx, accountInfo, isAdmin);
68-
} catch (RuntimeException ex) {
69-
ex.printStackTrace();
70-
ctx.redirect("/login/failed");
71-
}
54+
String sub = ctx.formParam("sub");
55+
if (sub == null || sub.isBlank()) throw new RuntimeException("Please provide a username");
56+
String name = sub;
57+
String email = sub + "@example.com";
58+
String organisation = "Example organisation";
59+
OpenIdInfo localInfo = new OpenIdInfo(sub, name, email, organisation);
60+
61+
AccountInfo accountInfo = new PostgrestAccount().account(localInfo, OpenidProvider.local);
62+
boolean isAdmin = isAdmin(email);
63+
createAndSetToken(ctx, accountInfo, isAdmin);
7264
});
7365
}
7466

7567
if (Config.isSurfConextEnabled()) {
7668
app.post("/login/surfconext", ctx -> {
77-
try {
78-
String code = ctx.formParam("code");
79-
String redirectUrl = Config.surfconextRedirect();
80-
OpenIdInfo surfconextInfo = new SurfconextLogin(code, redirectUrl).openidInfo();
81-
82-
if (!userIsAllowed(surfconextInfo)) {
83-
throw new RuntimeException("User is not whitelisted");
84-
}
85-
86-
AccountInfo accountInfo = new PostgrestAccount().account(surfconextInfo, OpenidProvider.surfconext);
87-
String email = surfconextInfo.email();
88-
boolean isAdmin = isAdmin(email);
89-
createAndSetToken(ctx, accountInfo, isAdmin);
90-
} catch (RuntimeException ex) {
91-
ex.printStackTrace();
92-
ctx.redirect("/login/failed");
69+
String code = ctx.formParam("code");
70+
String redirectUrl = Config.surfconextRedirect();
71+
OpenIdInfo surfconextInfo = new SurfconextLogin(code, redirectUrl).openidInfo();
72+
73+
if (!userIsAllowed(surfconextInfo)) {
74+
throw new RsdAuthenticationException("Your email address (" + surfconextInfo.email() + ") is not whitelisted.");
9375
}
76+
77+
AccountInfo accountInfo = new PostgrestAccount().account(surfconextInfo, OpenidProvider.surfconext);
78+
String email = surfconextInfo.email();
79+
boolean isAdmin = isAdmin(email);
80+
createAndSetToken(ctx, accountInfo, isAdmin);
9481
});
9582
}
9683

9784
if (Config.isHelmholtzEnabled()) {
9885
app.get("/login/helmholtzaai", ctx -> {
99-
try {
100-
String code = ctx.queryParam("code");
101-
String redirectUrl = Config.helmholtzAaiRedirect();
102-
OpenIdInfo helmholtzInfo = new HelmholtzAaiLogin(code, redirectUrl).openidInfo();
103-
104-
if (!userIsAllowed(helmholtzInfo)) {
105-
throw new RuntimeException("User is not whitelisted");
106-
}
107-
108-
AccountInfo accountInfo = new PostgrestAccount().account(helmholtzInfo, OpenidProvider.helmholtz);
109-
String email = helmholtzInfo.email();
110-
boolean isAdmin = isAdmin(email);
111-
createAndSetToken(ctx, accountInfo, isAdmin);
112-
} catch (RuntimeException ex) {
113-
ex.printStackTrace();
114-
ctx.redirect("/login/failed");
86+
String code = ctx.queryParam("code");
87+
String redirectUrl = Config.helmholtzAaiRedirect();
88+
OpenIdInfo helmholtzInfo = new HelmholtzAaiLogin(code, redirectUrl).openidInfo();
89+
90+
if (!userIsAllowed(helmholtzInfo)) {
91+
throw new RsdAuthenticationException("Your email address (" + helmholtzInfo.email() + ") is not whitelisted.");
11592
}
93+
94+
AccountInfo accountInfo = new PostgrestAccount().account(helmholtzInfo, OpenidProvider.helmholtz);
95+
String email = helmholtzInfo.email();
96+
boolean isAdmin = isAdmin(email);
97+
createAndSetToken(ctx, accountInfo, isAdmin);
11698
});
11799
}
118100

119101
if (Config.isOrcidEnabled()) {
120102
app.get("/login/orcid", ctx -> {
121-
try {
122-
String code = ctx.queryParam("code");
123-
String redirectUrl = Config.orcidRedirect();
124-
OpenIdInfo orcidInfo = new OrcidLogin(code, redirectUrl).openidInfo();
125-
126-
AccountInfo accountInfo = new PostgrestCheckOrcidWhitelistedAccount(new PostgrestAccount()).account(orcidInfo, OpenidProvider.orcid);
127-
String email = orcidInfo.email();
128-
boolean isAdmin = isAdmin(email);
129-
createAndSetToken(ctx, accountInfo, isAdmin);
130-
} catch (RuntimeException ex) {
131-
ex.printStackTrace();
132-
ctx.redirect("/login/failed");
133-
}
103+
String code = ctx.queryParam("code");
104+
String redirectUrl = Config.orcidRedirect();
105+
OpenIdInfo orcidInfo = new OrcidLogin(code, redirectUrl).openidInfo();
106+
107+
AccountInfo accountInfo = new PostgrestCheckOrcidWhitelistedAccount(new PostgrestAccount()).account(orcidInfo, OpenidProvider.orcid);
108+
String email = orcidInfo.email();
109+
boolean isAdmin = isAdmin(email);
110+
createAndSetToken(ctx, accountInfo, isAdmin);
134111
});
135112
}
136113

@@ -156,6 +133,17 @@ public static void main(String[] args) {
156133
ctx.status(400);
157134
ctx.json("{\"message\": \"invalid JWT\"}");
158135
});
136+
137+
app.exception(RsdAuthenticationException.class, (ex, ctx) -> {
138+
setLoginFailureCookie(ctx, ex.getMessage());
139+
ctx.redirect("/login/failed");
140+
});
141+
142+
app.exception(RuntimeException.class, (ex, ctx) -> {
143+
ex.printStackTrace();
144+
setLoginFailureCookie(ctx, "Something unexpected went wrong, please try again or contact us.");
145+
ctx.redirect("/login/failed");
146+
});
159147
}
160148

161149
static boolean isAdmin(String email) {
@@ -173,6 +161,10 @@ static void setJwtCookie(Context ctx, String token) {
173161
ctx.header("Set-Cookie", "rsd_token=" + token + "; Secure; HttpOnly; Path=/; SameSite=Lax; Max-Age=" + ONE_HOUR_IN_SECONDS);
174162
}
175163

164+
static void setLoginFailureCookie(Context ctx, String message) {
165+
ctx.header("Set-Cookie", "rsd_login_failure_message=" + message + "; Secure; Path=/login/failed; SameSite=Lax; Max-Age=" + ONE_MINUTE_IN_SECONDS);
166+
}
167+
176168
static void setRedirectFromCookie(Context ctx) {
177169
String returnPath = ctx.cookie("rsd_pathname");
178170
if (returnPath != null && !returnPath.isBlank()) {

authentication/src/main/java/nl/esciencecenter/rsd/authentication/PostgrestCheckOrcidWhitelistedAccount.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public AccountInfo account(OpenIdInfo openIdInfo, OpenidProvider provider) {
3737
JwtCreator jwtCreator = new JwtCreator(Config.jwtSigningSecret());
3838
String token = jwtCreator.createAdminJwt();
3939
String response = PostgrestAccount.getAsAdmin(queryUri, token);
40-
if (!orcidInResponse(orcid, response)) throw new AuthenticationException("Your ORCID (" + orcid + ") is not whitelisted.");
40+
if (!orcidInResponse(orcid, response)) throw new RsdAuthenticationException("Your ORCID (" + orcid + ") is not whitelisted.");
4141

4242
return origin.account(openIdInfo, provider);
4343
}

authentication/src/main/java/nl/esciencecenter/rsd/authentication/AuthenticationException.java renamed to authentication/src/main/java/nl/esciencecenter/rsd/authentication/RsdAuthenticationException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
package nl.esciencecenter.rsd.authentication;
77

8-
public class AuthenticationException extends RuntimeException {
8+
public class RsdAuthenticationException extends RuntimeException {
99

10-
public AuthenticationException(String message) {
10+
public RsdAuthenticationException(String message) {
1111
super(message);
1212
}
1313
}

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ services:
9292
# dockerfile to use for build
9393
dockerfile: Dockerfile
9494
# update version number to correspond to frontend/package.json
95-
image: rsd/frontend:1.6.6
95+
image: rsd/frontend:1.9.1
9696
environment:
9797
# it uses values from .env file
9898
- POSTGREST_URL
@@ -202,7 +202,7 @@ services:
202202
- DUID
203203
- DGID
204204
# update version number to correspond to frontend/package.json
205-
image: rsd/frontend-dev:1.6.4
205+
image: rsd/frontend-dev:1.9.1
206206
ports:
207207
- "3000:3000"
208208
- "9229:9229"

frontend/auth/locationCookie.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export function saveLocationCookie() {
1717
case '/login':
1818
case '/logout':
1919
case '/login/local':
20+
case '/login/failed':
2021
break
2122
case '/':
2223
// root is send to /software

frontend/components/feedback/FeedbackPanelButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Divider from '@mui/material/Divider'
1111
import getBrowser from '~/utils/getBrowser'
1212

1313
export default function FeedbackPanelButton(
14-
{feedback_email, issues_page_url, closeFeedbackPanel}: { feedback_email: string, issues_page_url:string, closeFeedbackPanel?: () => void }
14+
{feedback_email, issues_page_url, closeFeedbackPanel}: {feedback_email: string, issues_page_url:string, closeFeedbackPanel?: () => void }
1515
) {
1616

1717
const [text, setText] = useState('')

frontend/components/form/AsyncAutocompleteSC.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,14 @@ export default function AsyncAutocompleteSC<T>({status, options, config,
144144
// because search text is usually not identical to selected item
145145
// we ignore onInputChange event when reason==='reset'
146146
setInputValue(newInputValue)
147-
147+
// if user removes all input and onClear is provided
148+
// we trigger on clear event. In addition, in freeSolo
149+
// the icon is present that activates reason===clear
150+
if (reason === 'input' && newInputValue === '' && onClear) {
151+
// console.log('Call on clear event')
152+
// issue clear attempt
153+
onClear()
154+
}
148155
// we start new search if processing
149156
// is not empty we should reset it??
150157
if (processing !== '') {

frontend/components/keyword/FindKeyword.test.tsx

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ const props = {
3131
onCreate:mockCreate
3232
}
3333

34+
afterEach(() => {
35+
jest.runOnlyPendingTimers()
36+
// jest.useRealTimers()
37+
})
3438

3539
// this test needs to be first to return mocked non-resolving promise
3640
it('calls seach Fn and renders the loader', async () => {
@@ -54,9 +58,9 @@ it('calls seach Fn and renders the loader', async () => {
5458
})
5559

5660
await waitFor(() => {
57-
// validate that searchFn is called once
58-
expect(mockSearch).toHaveBeenCalledTimes(1)
59-
// is called with seachFor term
61+
// validate that searchFn is called twice (first on load then on search)
62+
expect(mockSearch).toHaveBeenCalledTimes(2)
63+
// last called with seachFor term
6064
expect(mockSearch).toHaveBeenCalledWith({searchFor})
6165
// check if loader is present
6266
const loader = screen.getByTestId('circular-loader')
@@ -80,8 +84,10 @@ it('renders component with label, help and input with role comobox', () => {
8084
it('offer Add option when search has no results', async() => {
8185
// prepare
8286
jest.useFakeTimers()
83-
// resolve with no options
84-
mockSearch.mockResolvedValueOnce([])
87+
// resolve with no options twice (on load and on search)
88+
mockSearch
89+
.mockResolvedValueOnce([])
90+
.mockResolvedValueOnce([])
8591
// render component
8692
render(<FindKeyword {...props} />)
8793

@@ -113,11 +119,15 @@ it('DOES NOT offer Add option when search return result that match', async () =>
113119
const searchFor = 'test'
114120
const searchCnt = 123
115121
// resolve with no options
116-
mockSearch.mockResolvedValueOnce([{
117-
id: '123123',
118-
keyword: searchFor,
119-
cnt: searchCnt
120-
}])
122+
mockSearch
123+
// intial call on load
124+
.mockResolvedValueOnce([])
125+
// search call
126+
.mockResolvedValueOnce([{
127+
id: '123123',
128+
keyword: searchFor,
129+
cnt: searchCnt
130+
}])
121131

122132
// render component
123133
render(<FindKeyword {...props} />)
@@ -152,7 +162,10 @@ it('calls onCreate method with string value to add new option', async() => {
152162
// prepare
153163
jest.useFakeTimers()
154164
// resolve with no options
155-
mockSearch.mockResolvedValueOnce([])
165+
mockSearch
166+
// intial call on load
167+
.mockResolvedValueOnce([])
168+
.mockResolvedValueOnce([])
156169
// render component
157170
render(<FindKeyword {...props} />)
158171

@@ -192,7 +205,11 @@ it('calls onAdd method to add option to selection', async() => {
192205
cnt: searchCnt
193206
}
194207
// resolve with no options
195-
mockSearch.mockResolvedValueOnce([mockOption])
208+
mockSearch
209+
// intial call on load
210+
.mockResolvedValueOnce([])
211+
// search call
212+
.mockResolvedValueOnce([mockOption])
196213
// render component
197214
render(<FindKeyword {...props} />)
198215

0 commit comments

Comments
 (0)