Skip to content

Commit 2985070

Browse files
refactor(calm-hub-ui): improve authorization header handling when hosted behind ADC/reverse proxy (#1780)
* refactor(calm-hub-ui): improve authorization header handling when hosted behind ADC/reverse proxy set AUTH_SERVICE_OIDC_ENABLE to true when backend is running with secure profile * docs(calm-hub): update setup instructions for secure profile * docs(calm-hub): update setup instructions for secure profile * refactor(calm-hub): addressing review comments * docs(calm-hub): Update readme instructions to launch calm-hub using a secure profile
1 parent 07c70c3 commit 2985070

File tree

8 files changed

+83
-53
lines changed

8 files changed

+83
-53
lines changed

calm-hub-ui/src/authService.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { UserManager, Log, User } from 'oidc-client';
22

33
const config = {
4-
authority: 'https://localhost:9443/realms/calm-hub-realm',
4+
authority: 'https://calm-hub.finos.org:9443/realms/calm-hub-realm',
55
client_id: 'calm-hub-authz-code',
66
redirect_uri: window.location.origin,
77
response_type: 'code',
@@ -12,9 +12,20 @@ const config = {
1212
loadUserInfo: true,
1313
};
1414

15+
//Set AUTH_SERVICE_OIDC_ENABLE to true only when the backend is running with a secure profile and is NOT behind an ADC/Reverse-Proxy that handles user authentication.
16+
export const AUTH_SERVICE_OIDC_ENABLE: boolean = false;
1517
let userManager: UserManager | null = null;
16-
const isHttps = window.location.protocol === 'https:';
17-
if (isHttps) {
18+
19+
export function isAuthServiceEnabled(): boolean {
20+
const oidcEnabled = AUTH_SERVICE_OIDC_ENABLE;
21+
const isHttps =
22+
typeof window !== 'undefined' &&
23+
typeof window.location !== 'undefined' &&
24+
window.location.protocol === 'https:';
25+
return (oidcEnabled && isHttps);
26+
}
27+
28+
if (isAuthServiceEnabled()) {
1829
userManager = new UserManager(config);
1930
Log.logger = console;
2031
Log.level = Log.INFO;
@@ -56,7 +67,7 @@ export async function clearSession(): Promise<void> {
5667
}
5768

5869
export async function getToken(): Promise<string> {
59-
if (!isHttps) {
70+
if (!AUTH_SERVICE_OIDC_ENABLE) {
6071
return '';
6172
}
6273
const user = await userManager?.getUser();
@@ -76,6 +87,15 @@ export async function getToken(): Promise<string> {
7687
return '';
7788
}
7889

90+
export async function getAuthHeaders(): Promise<HeadersInit> {
91+
const accessToken = await getToken();
92+
const headers: HeadersInit = {};
93+
if (accessToken) {
94+
headers['Authorization'] = `Bearer ${accessToken}`;
95+
}
96+
return headers;
97+
}
98+
7999
export async function checkAuthorityService(): Promise<boolean> {
80100
try {
81101
const response = await fetch(config.authority, { method: 'HEAD' });
@@ -93,4 +113,6 @@ export const authService = {
93113
logout,
94114
clearSession,
95115
getToken,
116+
getAuthHeaders,
117+
isAuthServiceEnabled,
96118
};

calm-hub-ui/src/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import './index.css';
22
import React from 'react';
33
import ReactDOM from 'react-dom/client';
44
import ProtectedRoute from './ProtectedRoute.js';
5-
import { authService } from './authService.js';
5+
import { isAuthServiceEnabled, authService } from './authService.js';
66
import App from './App.js';
77

88
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
@@ -19,11 +19,11 @@ const LogoutButton: React.FC = () => {
1919
);
2020
};
2121

22-
const isHttps = window.location.protocol === 'https:';
22+
const isAuthenticationEnabled = isAuthServiceEnabled();
2323

2424
root.render(
2525
<React.StrictMode>
26-
{isHttps ? (
26+
{isAuthenticationEnabled ? (
2727
<ProtectedRoute>
2828
<App />
2929
<LogoutButton />

calm-hub-ui/src/service/adr-service/adr-service.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import axios, { AxiosInstance } from 'axios';
2-
import { getToken } from '../../authService.js';
2+
import { getAuthHeaders } from '../../authService.js';
33
import { CalmAdrMeta } from '@finos/calm-shared/src/view-model/adr.js';
44

55
export class AdrService {
@@ -16,11 +16,10 @@ export class AdrService {
1616
* Fetch adr IDs for a given namespace and set them using the provided setter function.
1717
*/
1818
async fetchAdrIDs(namespace: string) : Promise<number[]> {
19+
const headers = await getAuthHeaders();
1920
return this.ax
2021
.get(`/calm/namespaces/${namespace}/adrs`, {
21-
headers: {
22-
Authorization: `Bearer ${await getToken()}`,
23-
},
22+
headers,
2423
})
2524
.then((res) => res.data.values)
2625
.catch((error) => {
@@ -34,11 +33,10 @@ export class AdrService {
3433
* Fetch revisions for a given namespace and adr ID and set them using the provided setter function.
3534
*/
3635
async fetchAdrRevisions(namespace: string, adrID: string): Promise<number[]> {
36+
const headers = await getAuthHeaders();
3737
return this.ax
3838
.get(`/calm/namespaces/${namespace}/adrs/${adrID}/revisions`, {
39-
headers: {
40-
Authorization: `Bearer ${await getToken()}`,
41-
},
39+
headers,
4240
})
4341
.then((res) => res.data.values)
4442
.catch((error) => {
@@ -52,11 +50,10 @@ export class AdrService {
5250
* Fetch a specific adr by namespace, adr ID, and revision, and set it using the provided setter function.
5351
*/
5452
async fetchAdr(namespace: string, adrID: string, revision: string): Promise<CalmAdrMeta> {
53+
const headers = await getAuthHeaders();
5554
return this.ax
5655
.get(`/calm/namespaces/${namespace}/adrs/${adrID}/revisions/${revision}`, {
57-
headers: {
58-
Authorization: `Bearer ${await getToken()}`,
59-
},
56+
headers,
6057
})
6158
.then((res) => res.data)
6259
.catch((error) => {

calm-hub-ui/src/service/calm-service.tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { Data } from '../model/calm.js';
2-
import { getToken } from '../authService.js';
2+
import { getAuthHeaders } from '../authService.js';
33

44
/**
55
* Fetch namespaces and set them using the provided setter function.
66
*/
77
export async function fetchNamespaces(setNamespaces: (namespaces: string[]) => void) {
88
try {
9-
const accessToken = await getToken();
9+
const headers = await getAuthHeaders();
1010
const res = await fetch('/calm/namespaces', {
1111
method: 'GET',
12-
headers: { Authorization: `Bearer ${accessToken}` },
12+
headers,
1313
});
1414
const data = await res.json();
1515
setNamespaces(data.values);
@@ -26,10 +26,10 @@ export async function fetchPatternIDs(
2626
setPatternIDs: (patternIDs: string[]) => void
2727
) {
2828
try {
29-
const accessToken = await getToken();
29+
const headers = await getAuthHeaders();
3030
const res = await fetch(`/calm/namespaces/${namespace}/patterns`, {
3131
method: 'GET',
32-
headers: { Authorization: `Bearer ${accessToken}` },
32+
headers,
3333
});
3434
const data = await res.json();
3535
setPatternIDs(data.values.map((num: number) => num.toString()));
@@ -43,10 +43,10 @@ export async function fetchPatternIDs(
4343
*/
4444
export async function fetchFlowIDs(namespace: string, setFlowIDs: (flowIDs: string[]) => void) {
4545
try {
46-
const accessToken = await getToken();
46+
const headers = await getAuthHeaders();
4747
const res = await fetch(`/calm/namespaces/${namespace}/flows`, {
4848
method: 'GET',
49-
headers: { Authorization: `Bearer ${accessToken}` },
49+
headers,
5050
});
5151
const data = await res.json();
5252
setFlowIDs(data.values.map((id: number) => id.toString()));
@@ -64,10 +64,10 @@ export async function fetchPatternVersions(
6464
setVersions: (versions: string[]) => void
6565
) {
6666
try {
67-
const accessToken = await getToken();
67+
const headers = await getAuthHeaders();
6868
const res = await fetch(`/calm/namespaces/${namespace}/patterns/${patternID}/versions`, {
6969
method: 'GET',
70-
headers: { Authorization: `Bearer ${accessToken}` },
70+
headers,
7171
});
7272
const data = await res.json();
7373
setVersions(data.values);
@@ -85,10 +85,10 @@ export async function fetchFlowVersions(
8585
setFlowVersions: (flowVersions: string[]) => void
8686
) {
8787
try {
88-
const accessToken = await getToken();
88+
const headers = await getAuthHeaders();
8989
const res = await fetch(`/calm/namespaces/${namespace}/flows/${flowID}/versions`, {
9090
method: 'GET',
91-
headers: { Authorization: `Bearer ${accessToken}` },
91+
headers,
9292
});
9393
const data = await res.json();
9494
setFlowVersions(data.values);
@@ -107,12 +107,12 @@ export async function fetchPattern(
107107
setPattern: (pattern: Data) => void
108108
) {
109109
try {
110-
const accessToken = await getToken();
110+
const headers = await getAuthHeaders();
111111
const res = await fetch(
112112
`/calm/namespaces/${namespace}/patterns/${patternID}/versions/${version}`,
113113
{
114114
method: 'GET',
115-
headers: { Authorization: `Bearer ${accessToken}` },
115+
headers,
116116
}
117117
);
118118
const response = await res.json();
@@ -142,12 +142,12 @@ export async function fetchFlow(
142142
setFlow: (flow: Data) => void
143143
) {
144144
try {
145-
const accessToken = await getToken();
145+
const headers = await getAuthHeaders();
146146
const res = await fetch(
147147
`/calm/namespaces/${namespace}/flows/${flowID}/versions/${version}`,
148148
{
149149
method: 'GET',
150-
headers: { Authorization: `Bearer ${accessToken}` },
150+
headers,
151151
}
152152
);
153153
const response = await res.json();
@@ -175,10 +175,10 @@ export async function fetchArchitectureIDs(
175175
setArchitectureIDs: (architectureIDs: string[]) => void
176176
) {
177177
try {
178-
const accessToken = await getToken();
178+
const headers = await getAuthHeaders();
179179
const res = await fetch(`/calm/namespaces/${namespace}/architectures`, {
180180
method: 'GET',
181-
headers: { Authorization: `Bearer ${accessToken}` },
181+
headers,
182182
});
183183
const data = await res.json();
184184
setArchitectureIDs(data.values.map((id: number) => id.toString()));
@@ -196,12 +196,12 @@ export async function fetchArchitectureVersions(
196196
setVersions: (versions: string[]) => void
197197
) {
198198
try {
199-
const accessToken = await getToken();
199+
const headers = await getAuthHeaders();
200200
const res = await fetch(
201201
`/calm/namespaces/${namespace}/architectures/${architectureID}/versions`,
202202
{
203203
method: 'GET',
204-
headers: { Authorization: `Bearer ${accessToken}` },
204+
headers,
205205
}
206206
);
207207
const data = await res.json();
@@ -221,12 +221,12 @@ export async function fetchArchitecture(
221221
setArchitecture: (architecture: Data) => void
222222
) {
223223
try {
224-
const accessToken = await getToken();
224+
const headers = await getAuthHeaders();
225225
const res = await fetch(
226226
`/calm/namespaces/${namespace}/architectures/${architectureID}/versions/${version}`,
227227
{
228228
method: 'GET',
229-
headers: { Authorization: `Bearer ${accessToken}` },
229+
headers,
230230
}
231231
);
232232
const response = await res.json();

calm-hub/README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,14 @@ From the `calm-hub` directory
124124

125125
From the `keycloak-dev` directory in `calm-hub`
126126

127-
Create certs for KeyCloak:
127+
Create self-signed X.509 certificate for Keycloak:
128128

129129
```shell
130130
mkdir ./certs &&
131131
openssl req -x509 -newkey rsa:2048 \
132132
-keyout ./certs/key.pem \
133133
-out ./certs/cert.pem -days 90 -nodes \
134-
-subj "/C=GB/ST=England/L=Manchester/O=finos/OU=Technology/CN=idp.finos.org"
134+
-subj "/C=GB/ST=England/L=Manchester/O=finos/OU=Technology/CN=calm-hub.finos.org"
135135
```
136136

137137
Launch KeyCloak:
@@ -142,12 +142,12 @@ docker-compose up
142142
- Open KeyCloak UI: https://localhost:9443, login with admin user.
143143
- Switch realm from `master` to `calm-hub-realm`.
144144
- You can find a `demo` user with a temporary credentials under `calm-hub-realm` realm.
145-
- During the local development, the `demo` user you can use to authenticate with `keycloak-dev` when you integrate the `calm-ui` with `authorization-code` flow type.
145+
- During local development, you can use the `demo` user to authenticate with `keycloak-dev` when integrating calm-ui using the `authorization code flow`.
146146

147147
#### Server Side with secure profile
148148

149149
From the `calm-hub` directory
150-
1. Create a server side certificates
150+
1. Create a server-side certificate
151151
```shell
152152
openssl req -x509 -newkey rsa:2048 \
153153
-keyout ./src/main/resources/key.pem \
@@ -156,7 +156,11 @@ From the `calm-hub` directory
156156
```
157157
2. `../mvnw package`
158158
3. `../mvnw quarkus:dev -Dquarkus.profile=secure`
159-
4. Open Calm UI: https://localhost:8443
159+
4. When using a self-signed certificate, you have two options to avoid the `No name matching localhost found` CertificateException in the backend.
160+
1. Add a host entry in `/etc/hosts` file, for example `127.0.0.1 calm-hub.finos.org`
161+
2. Alternatively, create the self-signed certificate with localhost as the CN or SAN.
162+
5. Some browsers may block `.well-known` endpoints that use self-signed certificates (e.g., https://calm-hub.finos.org:9443/realms/calm-hub-realm/.well-known/openid-configuration). Ensure these endpoints are accessible in your browser before accessing `calm-hub-ui`.
163+
6. Open Calm UI at the URL matching your self-signed certificate’s CN: https://calm-hub.finos.org:8443 or https://localhost:8443.
160164

161165
### UI with hot reload (from src/main/webapp)
162166

calm-hub/keycloak-dev/device-code-flow.sh

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
CLIENT_ID="calm-hub-admin-app"
44
SCOPE="namespace:admin"
5-
DEVICE_AUTH_ENDPOINT="https://localhost:9443/realms/calm-hub-realm/protocol/openid-connect/auth/device"
5+
DEVICE_AUTH_ENDPOINT="https://calm-hub.finos.org:9443/realms/calm-hub-realm/protocol/openid-connect/auth/device"
66
DEVICE_AUTH_RESPONSE=$(curl --insecure -X POST \
77
-H "Content-Type: application/x-www-form-urlencoded" \
88
-d "client_id=$CLIENT_ID" -d "scope=$SCOPE" \
99
$DEVICE_AUTH_ENDPOINT)
1010

11+
BLUE="\033[0;34m"
12+
YELLOW="\033[0;33m"
13+
NC="\033[0m"
14+
1115
# Extract values from the device auth response.
1216
DEVICE_CODE=$(echo "$DEVICE_AUTH_RESPONSE" | jq -r '.device_code')
1317
USER_CODE=$(echo "$DEVICE_AUTH_RESPONSE" | jq -r '.user_code')
@@ -16,10 +20,12 @@ VERIFICATION_URI_COMPLETE=$(echo "$DEVICE_AUTH_RESPONSE" | jq -r '.verification_
1620
EXPIRES_IN=$(echo "$DEVICE_AUTH_RESPONSE" | jq -r '.expires_in')
1721
INTERVAL=$(echo "$DEVICE_AUTH_RESPONSE" | jq -r '.interval')
1822

19-
echo -e "\nOpen the link in a browser \033[3m[$VERIFICATION_URI]\033[0m, and authenticate with the UserCode:[$USER_CODE] \n the associated device code for this request will expires in $EXPIRES_IN seconds.\n"
23+
echo -e "${YELLOW}\nThe user you use to authenticate must have the ${SCOPE} scope; otherwise, calm-hub will respond with a 401 or 403 error. For local development with a secure profile, you can use the *demo_admin* user, which is already created in Keycloak.\n${NC}"
24+
25+
echo -e "\nOpen the link in a browser ${BLUE}[$VERIFICATION_URI]${NC}, and authenticate with the UserCode:[$USER_CODE],the associated device code for this request will expires in ${EXPIRES_IN} seconds.\n"
2026

2127
# Poll the token endpoint
22-
TOKEN_URL="https://localhost:9443/realms/calm-hub-realm/protocol/openid-connect/token"
28+
TOKEN_URL="https://calm-hub.finos.org:9443/realms/calm-hub-realm/protocol/openid-connect/token"
2329
ACCESS_TOKEN=""
2430
POLL_INTERVAL=15 #Seconds
2531

@@ -57,21 +63,21 @@ poll_token
5763
echo -e "\nPositive Case: Press enter to create a sample user-access for finos resources."
5864
read
5965
if [[ -n $ACCESS_TOKEN ]]; then
60-
curl -X POST --insecure -v "https://localhost:8443/calm/namespaces/finos/user-access" \
66+
curl -X POST --insecure -v "https://calm-hub.finos.org:8443/calm/namespaces/finos/user-access" \
6167
-H "Content-Type: application/json" \
6268
-H "Authorization: Bearer $ACCESS_TOKEN" \
6369
-d '{ "namespace": "finos", "resourceType": "patterns", "permission": "read", "username": "demo" }'
6470

6571
echo -e "\nPositive Case: Press enter to get list of user-access details associated to namespace:finos"
6672
read
67-
curl --insecure -v "https://localhost:8443/calm/namespaces/finos/user-access" \
73+
curl --insecure -v "https://calm-hub.finos.org:8443/calm/namespaces/finos/user-access" \
6874
-H "Content-Type: application/json" \
6975
-H "Authorization: Bearer $ACCESS_TOKEN"
7076

7177
echo
7278
echo -e "\nFailure Case: Press enter to create a sample user-access for traderx namespace."
7379
read
74-
curl -X POST --insecure -v "https://localhost:8443/calm/namespaces/traderx/user-access" \
80+
curl -X POST --insecure -v "https://calm-hub.finos.org:8443/calm/namespaces/traderx/user-access" \
7581
-H "Content-Type: application/json" \
7682
-H "Authorization: Bearer $ACCESS_TOKEN" \
7783
-d '{ "namespace": "traderx", "resourceType": "patterns", "permission": "read", "username": "demo" }'

0 commit comments

Comments
 (0)